node-kobold-control
Version:
Vorwerk Kobold API for VR300
632 lines (568 loc) • 26.1 kB
JavaScript
var api = require(__dirname + '/api');
var crypto = require('crypto');
function Robot(name, serial, secret, token) {
this._nucleoBaseUrl = 'https://nucleo.neatocloud.com:4443/vendors/neato/robots/';
//this._beehiveBaseUrl = 'https://beehive.neatocloud.com/users/me/robots/';
this._beehiveBaseUrl = 'https://beehive.ksecosys.com/users/me/robots/'
this._vorwerkBaseUrl = 'https://nucleo.ksecosys.com/vendors/vorwerk/robots/';
this.name = name;
this._serial = serial;
this._secret = secret;
this._token = token;
//updated when getState() is called
this.availableServices = null;
this.isBinFull = null;
this.isCharging = null;
this.isDocked = null;
this.isScheduleEnabled = null;
this.dockHasBeenSeen = null;
this.charge = null;
this.canStart = null;
this.canStop = null;
this.canPause = null;
this.canResume = null;
this.canGoToBase = null;
this.eco = null;
this.noGoLines = null;
this.navigationMode = null;
this.spotWidth = null;
this.spotHeight = null;
this.spotRepeat = null;
this.cleaningBoundaryId = null;
}
Robot.prototype.getState = function getState(callback) {
doAction(this, 'getRobotState', null, (function (error, result) {
if (typeof callback === 'function') {
if (result === undefined) {
if (!error) {
error = 'no result';
}
callback(error, null);
} else if (result && 'message' in result) {
callback(result.message, result);
} else {
this.availableServices = result.availableServices;
this.isBinFull = result.alert == "dustbin_full";
this.isCharging = result.details.isCharging;
this.isDocked = result.details.isDocked;
this.isScheduleEnabled = result.details.isScheduleEnabled;
this.dockHasBeenSeen = result.details.dockHasBeenSeen;
this.charge = result.details.charge;
this.canStart = result.availableCommands.start;
this.canStop = result.availableCommands.stop;
this.canPause = result.availableCommands.pause;
this.canResume = result.availableCommands.resume;
this.canGoToBase = result.availableCommands.goToBase;
// Read cleaning parameters from last run
this.eco = result.cleaning.mode === 1;
// 4: house cleaning with noGoLines
if (result.cleaning.category === 4) {
this.noGoLines = true;
}
// 2: house cleaning without noGoLines
else if (result.cleaning.category === 2) {
this.noGoLines = false;
}
// 1+3: spot+manual cleaning. Set nogolines to default = false only if not set already
else if (this.noGoLines === null) {
this.noGoLines = false;
}
this.navigationMode = result.cleaning.navigationMode;
this.spotWidth = result.cleaning.spotWidth;
this.spotHeight = result.cleaning.spotHeight;
this.spotRepeat = result.cleaning.modifier === 2;
this.cleaningBoundaryId = result.cleaning.boundaryId;
callback(error, result);
}
}
}).bind(this));
};
Robot.prototype.getSchedule = function getSchedule(detailed, callback) {
if (typeof detailed === 'function') {
callback = detailed;
detailed = false;
}
doAction(this, 'getSchedule', null, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'data' in result && (detailed !== undefined) && detailed ) {
callback(null, result.data);
} else if (result && 'data' in result && 'enabled' in result.data) {
callback(null, result.data.enabled);
} else {
callback(result && 'message' in result ? result.message : 'failed', result);
}
}
});
};
let timeValidator = new RegExp(/^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$/);
Robot.prototype.setSchedule = function setSchedule(schedule, callback) {
let error = '';
let debug = '';
if ((schedule === undefined) || (schedule === null) || (typeof schedule !== 'object') || (!Object.keys(schedule).length)) {
error = 'Wrong type of schedule object!';
}
else if (!this.availableServices.hasOwnProperty('schedule')) {
error = 'Schedule is not suported by robot!';
}
else if (!schedule.hasOwnProperty('type') || (! Number.isInteger(schedule.type)) || (schedule.type !== 1)) {
error = 'Mandatory property type is missed or mailformed!';
}
else if (!schedule.hasOwnProperty('events') || (!Array.isArray(schedule.events)) || (schedule.events.length > 7)) {
error = 'Mandatory property events is missed or mailformed!';
}
else if (! schedule.events.every(function scheduleValidate (event) {
let eventLength = Object.keys(event).length;
//console.log() doesn't work under ioBroker, thats why such construction is used
//check maximum possible fields amount per type of schedule
debug += "\n" + '1 ' + JSON.stringify((((this.availableServices.schedule === 'minimal-1') && (eventLength > 2)) || ((this.availableServices.schedule === 'basic-1') && (eventLength > 3))
|| ((this.availableServices.schedule === 'basic-2') && (eventLength > 4)))) ;
if (((this.availableServices.schedule === 'minimal-1') && (eventLength > 2)) || ((this.availableServices.schedule === 'basic-1') && (eventLength > 3))
|| ((this.availableServices.schedule === 'basic-2') && (eventLength > 4))) return false;
//propery day is mandatory and has to be in between 0 and 6. The day of the week. 0 Sunday 1 Monday 2 Tuesday 3 Wednesday 4 Thursday 5 Friday 6 Saturday
debug += "\n" + ' 2 ' + JSON.stringify(((! event.hasOwnProperty('day')) || (! Number.isInteger(event.day)) || ( event.day < 0 ) || ( event.day > 6)));
if ((! event.hasOwnProperty('day')) || (! Number.isInteger(event.day)) || ( event.day < 0 ) || ( event.day > 6)) return false;
//property startTime is mandatory and has to be in 'HH:MM' format
debug += "\n" + ' 3 ' + JSON.stringify(((! event.hasOwnProperty('startTime')) || ( typeof event.startTime !== 'string') || ( ! event.startTime ) || (! timeValidator.test(event.startTime))));
if ((! event.hasOwnProperty('startTime')) || ( typeof event.startTime !== 'string') || ( ! event.startTime ) || (! timeValidator.test(event.startTime))) return false;
//property mode is not available on 'minimal-1' schedule type
debug += "\n" + ' 4 ' + JSON.stringify(((this.availableServices.schedule === 'minimal-1') && (event.hasOwnProperty('mode'))));
if ((this.availableServices.schedule === 'minimal-1') && (event.hasOwnProperty('mode'))) return false;
//but is required for basic-1 and basic-2
debug += "\n" + ' 5 ' + JSON.stringify((((this.availableServices.schedule === 'basic-1') || (this.availableServices.schedule === 'basic-1')) && (!event.hasOwnProperty('mode'))));
if (((this.availableServices.schedule === 'basic-1') || (this.availableServices.schedule === 'basic-1')) && (!event.hasOwnProperty('mode'))) return false;
//mode - the cleaning mode. 1 Eco 2 Normal.
debug += "\n" + ' 6 ' + JSON.stringify(((event.hasOwnProperty('mode')) && ((! Number.isInteger(event.mode)) || ( event.mode < 1 ) || ( event.mode > 2))));
if ((event.hasOwnProperty('mode')) && ((! Number.isInteger(event.mode)) || ( event.mode < 1 ) || ( event.mode > 2))) return false;
//initially setting boundaries is not supported
debug += "\n" + ' 7 ' + JSON.stringify((((this.availableServices.schedule === 'minimal-1') || (this.availableServices.schedule === 'basic-1')) && (event.hasOwnProperty('boundaryId'))));
if (((this.availableServices.schedule === 'minimal-1') || (this.availableServices.schedule === 'basic-1')) && (event.hasOwnProperty('boundaryId'))) return false;
// to process boundaries from ioBroker, as it not editable, and will be reused from original
//debug += "\n" + ' 8 ' + JSON.stringify((event.hasOwnProperty('boundaryId') && event.boundaryId ));
//if (event.hasOwnProperty('boundaryId') && event.boundaryId ) return false;
return true;
}, this)
) {
error = 'Wrong structure of the schedule! Please check!' + ' ' + debug;
}
if (error) {
if (typeof callback === 'function') {
callback(error, schedule);
}
return;
}
doAction(this, 'setSchedule', schedule, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'data' in result ) {
callback(null, result.data);
} else {
callback(result && 'message' in result ? result.message : 'failed', result);
}
}
});
};
Robot.prototype.enableSchedule = function enableSchedule(callback) {
doAction(this, 'enableSchedule', null, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
Robot.prototype.disableSchedule = function disableSchedule(callback) {
doAction(this, 'disableSchedule', null, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
Robot.prototype.sendToBase = function sendToBase(callback) {
doAction(this, 'sendToBase', null, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
Robot.prototype.stopCleaning = function stopCleaning(callback) {
doAction(this, 'stopCleaning', null, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
Robot.prototype.pauseCleaning = function pauseCleaning(callback) {
doAction(this, 'pauseCleaning', null, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
Robot.prototype.resumeCleaning = function resumeCleaning(callback) {
doAction(this, 'resumeCleaning', null, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
Robot.prototype.startSpotCleaning = function startSpotCleaning(eco, width, height, repeat, navigationMode, callback) {
if (typeof eco === 'function') {
callback = eco;
eco = this.eco;
width = this.spotWidth;
height = this.spotHeight;
repeat = this.spotRepeat;
navigationMode = this.navigationMode;
} else if (typeof width === 'function') {
callback = width;
width = this.spotWidth;
height = this.spotHeight;
repeat = this.spotRepeat;
navigationMode = this.navigationMode;
} else if (typeof height === 'function') {
callback = height;
height = this.spotHeight;
repeat = this.spotRepeat;
navigationMode = this.navigationMode;
} else if (typeof repeat === 'function') {
callback = repeat;
repeat = this.spotRepeat;
navigationMode = this.navigationMode;
} else if (typeof navigationMode === 'function') {
callback = navigationMode;
navigationMode = this.navigationMode;
}
if (typeof width !== 'number' || width < 100) {
width = this.width;
}
if (typeof height !== 'number' || height < 100) {
height = this.height;
}
if (typeof navigationMode !== 'number' || navigationMode < 1 || navigationMode > 2) {
navigationMode = this.navigationMode;
}
var params = {
category: 3, //1: manual, 2: house, 3: spot, 4: house with enabled nogolines
mode: eco ? 1 : 2, //1: eco, 2: turbo
modifier: repeat ? 2 : 1, //spot: clean spot 1 or 2 times
navigationMode: navigationMode, //1: normal, 2: extra care
spotWidth: width,
spotHeight: height
};
doAction(this, 'startCleaning', params, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
Robot.prototype.startManualCleaning = function startSpotCleaning(eco, navigationMode, callback) {
if (typeof eco === 'function') {
callback = eco;
eco = this.eco;
navigationMode = this.navigationMode;
} else if (typeof navigationMode === 'function') {
callback = navigationMode;
navigationMode = this.navigationMode;
}
var params = {
category: 1, //1: manual, 2: house, 3: spot, 4: house with enabled nogolines
mode: eco ? 1 : 2, //1: eco, 2: turbo
modifier: 1, //spot: clean spot 1 or 2 times
navigationMode: navigationMode //1: normal, 2: extra care
};
doAction(this, 'startCleaning', params, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
Robot.prototype.startCleaning = function startSpotCleaning(eco, navigationMode, noGoLines, callback) {
if (typeof eco === 'function') {
callback = eco;
eco = this.eco;
navigationMode = this.navigationMode;
noGoLines = this.noGoLines;
} else if (typeof navigationMode === 'function') {
callback = navigationMode;
navigationMode = this.navigationMode;
noGoLines = this.noGoLines;
} else if (typeof noGoLines === 'function') {
callback = noGoLines;
noGoLines = this.noGoLines;
}
var params = {
category: noGoLines ? 4 : 2, //1: manual, 2: house, 3: spot, 4: house with enabled nogolines and/or boundaries
mode: eco ? 1 : 2, //1: eco, 2: turbo
modifier: 1, //spot: clean spot 1 or 2 times
navigationMode: navigationMode //1: normal, 2: extra care
};
doAction(this, 'startCleaning', params, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
/**
* A default action callback
* @callback defaultActionCallback
* @param {string} [error] - An error message if an error occur
* @param {string|*} maps - "ok" on success or the request result if an error occur
*/
/**
* Start cleaning a specific boundary (of type polygone)
*
* @param {bool} eco - enable eco cleaning or do a turbo clean
* @param {bool} extraCare - enable extra care
* @param {string} boundaryId - A {@link Boundary} id to clean (uuid-v4)
* @param {defaultActionCallback} callback - a callback called when executing the action or an error
*/
Robot.prototype.startCleaningBoundary = function startSpotCleaning(eco, extraCare, boundaryId, callback) {
if (typeof eco === 'function') {
callback = eco;
eco = this.eco;
navigationMode = this.navigationMode;
noGoLines = this.noGoLines;
} else if (typeof navigationMode === 'function') {
callback = navigationMode;
navigationMode = this.navigationMode;
noGoLines = this.noGoLines;
} else if (typeof noGoLines === 'function') {
callback = noGoLines;
noGoLines = this.noGoLines;
}
var params = {
boundaryId: boundaryId, // Boundary to clean
category: 4, // 4: house with enabled nogolines and/or boundaries
mode: eco ? 1 : 2, //1: eco, 2: turbo
navigationMode: extraCare ? 2 : 1 //1: normal, 2: extra care
};
doAction(this, 'startCleaning', params, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
/**
* A Map
* @typedef {Object} Map
* @property {string} id - The title
* @property {string} name - The artist
* @property {string} raw_floor_map_url - url pointing to a png image of the raw floor map
* @property {string} url - url pointing to a png image of the floor map
* @property {number} url_valid_for_seconds - number of seconds the map urls are valid
*/
/**
* Callback for persistent maps.
*
* @callback getPersistentMapsCallback
* @param {string} [error] - An error message if an error occur
* @param {Map[]} maps - The list of {@link Map} for this robot
*/
/**
* Add two numbers together, then pass the results to a callback function.
*
* @param {getPersistentMapsCallback} callback - A callback called on receiving the maps or an error
*/
Robot.prototype.getPersistentMaps = function getPersistentMaps(callback) {
robotRequest(this, 'beehive', 'GET', '/persistent_maps', null, callback);
}
/**
* A Boundary (zone or no go line)
*
* @typedef {Object} Boundary
* @property {string} color - color hex code for a type polygone or '#000000' for a type polyline
* @property {bool} enabled - always true, unknown usage
* @property {string} id - boundary id (uuid-v4)
* @property {string} name - polygone name or empty string for a type polyline
* @property {number[]} [relevancy] - array of 2 number, center of a type polygone
* @property {string} type - either polyline (for a no go lines) or polygon (for a zone)
* @property {number[][]} vertices - array of array of two points, coordinates of the points
*/
/**
* Callback for map boundaries.
*
* @callback getMapBoundariesCallback
* @param {error} error - An integer.
* @param {Boundary[]|*} - An array of {@link Boundary} for the specified {@link Map} or the request result if an error occur
*/
/**
* Add two numbers together, then pass the results to a callback function.
*
* @param {string} mapId - An id from a {@link Map} to request its list of {@link Boundary}
* @param {getMapBoundariesCallback} callback - A callback called on receiving the boundaries or an error
*/
Robot.prototype.getMapBoundaries = function getMapBoundaries(mapId, callback) {
doAction(this, 'getMapBoundaries', { mapId: mapId }, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'data' in result && 'boundaries' in result.data) {
callback(null, { mapId: mapId, boundaries: result.data.boundaries });
} else {
callback(result && 'message' in result ? result.message : 'failed', result);
}
}
});
}
/**
* Callback when setting map boundaries.
*
* @callback setMapBoundariesCallback
* @param {error} error - An integer.
* @param {Boundary[]} - An array of {@link Boundary} for the specified {@link Map}
*/
/**
* Add two numbers together, then pass the results to a callback function.
*
* @param {string} mapId - An id from a {@link Map} to set its list of {@link Boundary}
* @param {Boundary[]} boundaries - List of all new {@link Boundary} for the given {@link Map}
* @param {setMapBoundariesCallback} callback - A callback called on receiving the boundaries or an error
*/
Robot.prototype.setMapBoundaries = function setMapBoundaries(mapId, boundaries, callback) {
doAction(this, 'setMapBoundaries', { mapId: mapId, boundaries: boundaries }, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'data' in result && 'boundaries' in result.data) {
callback(null, { mapId: mapId, boundaries: result.data.boundaries });
} else {
callback(result && 'message' in result ? result.message : 'failed', result);
}
}
});
}
/**
* A findMe callback
* @callback findMeCallback
* @param {string} [error] - An error message if an error occur
* @param {string} result - "ok" on success or the request result if an error occur
*/
/**
* Let the robot emit a sound and light to find him
*
* @param {findMeCallback} callback - a callback called when executing the action or an error
*/
Robot.prototype.findMe = function findMe(callback) {
var params = {};
doAction(this, 'findMe', params, function (error, result) {
if (typeof callback === 'function') {
if (error) {
callback(error, result);
} else if (result && 'result' in result && result.result === 'ok') {
callback(null, result.result);
} else {
callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result);
}
}
});
};
var robotMessagesRequestId = 1;
function doAction(robot, command, params, callback) {
var payload = {
reqId: robotMessagesRequestId++,
cmd: command
};
if (params) {
payload.params = params;
}
robotRequest(robot, 'vorwerk', 'POST', '/messages', payload, callback);
}
function robotRequest(robot, service, type, endpoint, payload, callback) {
if (robot._serial && robot._secret) {
payload = JSON.stringify(payload);
var date = new Date().toUTCString();
var data = [robot._serial.toLowerCase(), date, payload].join("\n");
var headers;
var url;
var cert = null;
if (service === 'vorwerk') {
var hmac = crypto.createHmac('sha256', robot._secret).update(data).digest('hex');
headers = {
Date: date,
Authorization: 'NEATOAPP ' + hmac
};
cert = __dirname + '/cert/neatocloud.com.crt';
url = robot._vorwerkBaseUrl + robot._serial + endpoint
}
else if (service === 'beehive') {
headers = {
Authorization: robot._token
};
url = robot._beehiveBaseUrl + robot._serial + endpoint
}
else {
callback('Service' + service + 'unknown');
}
api.request(url, payload, type, headers, cert, function (error, body) {
if (typeof callback === 'function') {
callback(error, body);
}
});
} else {
if (typeof callback === 'function') {
callback('no serial or secret');
}
}
}
module.exports = Robot;