├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── api.js ├── client.js └── robot.js ├── package-lock.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | [*.js] 5 | # Indentation and spacing 6 | indent_size = 4 7 | indent_style = space 8 | tab_width = 4 9 | 10 | # New line preferences 11 | end_of_line = crlf 12 | insert_final_newline = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /.vs 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Gruntfile.js 2 | tasks 3 | node_modules 4 | .idea 5 | .git 6 | /node_modules 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pmant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-botvac 2 | A node module for Neato Botvac Connected. 3 | Based on tomrosenbacks [PHP Port](https://github.com/tomrosenback/botvac) and [kanggurus work](https://github.com/kangguru/botvac) on the undocumented Neato API. 4 | 5 | ## Installation 6 | ```npm install node-botvac``` 7 | 8 | 9 | ## Usage Example 10 | ```Javascript 11 | var botvac = require('node-botvac'); 12 | 13 | var client = new botvac.Client(); 14 | //authorize 15 | client.authorize('email', 'password', false, function (error) { 16 | if (error) { 17 | console.log(error); 18 | return; 19 | } 20 | //get your robots 21 | client.getRobots(function (error, robots) { 22 | if (error) { 23 | console.log(error); 24 | return; 25 | } 26 | if (robots.length) { 27 | //do something 28 | robots[0].getState(function (error, result) { 29 | console.log(result); 30 | }); 31 | } 32 | }); 33 | }); 34 | ``` 35 | 36 | 37 | ## Client API 38 | * client.authorize() 39 | * client.getRobots() 40 | 41 | ------------------------------------------------------- 42 | 43 | ### client.authorize(email, password, force, callback) 44 | 45 | Login at the neato api. 46 | 47 | * `email` - your neato email 48 | * `password` - your neato passwort 49 | * `force` - force login if already authorized 50 | * `callback` - `function(error)` 51 | * `error` null if no error occurred 52 | 53 | ------------------------------------------------------- 54 | 55 | ### client.getRobots(callback) 56 | 57 | Returns an array containing your registered robots. 58 | 59 | * `callback` - `function(error, robots)` 60 | * `error` null if no error occurred 61 | * `robots` array - your robots 62 | 63 | 64 | 65 | ## Robot Properties 66 | * ```robot.name``` - nickname of this robot (cannot be changed) 67 | 68 | These properties will be updated every time robot.getState() is called: 69 | * ```robot.isBinFull``` boolean 70 | * ```robot.isCharging``` boolean 71 | * ```robot.isDocked``` boolean 72 | * ```robot.isScheduleEnabled``` boolean 73 | * ```robot.dockHasBeenSeen``` boolean 74 | * ```robot.charge``` number - charge in percent 75 | * ```robot.canStart``` boolean - robot is ready to start cleaning 76 | * ```robot.canStop``` boolean - cleaning can be stopped 77 | * ```robot.canPause``` boolean - cleaning can be paused 78 | * ```robot.canResume``` boolean - cleaning can be resumed 79 | * ```robot.canGoToBase``` boolean - robot can be sent to base 80 | * ```robot.eco``` boolean - set to true to clean in eco mode 81 | * ```robot.noGoLines``` boolean - set to true to enable noGoLines 82 | * ```robot.navigationMode``` number - 1: normal, 2: extra care (new models only) 83 | * ```robot.spotWidth``` number - width for spot cleaning in cm 84 | * ```robot.spotHeight``` number - height for spot cleaning in cm 85 | * ```robot.spotRepeat``` boolean - set to true to clean spot two times 86 | 87 | 88 | ## Robot API 89 | * robot.getState() 90 | * robot.getSchedule() 91 | * robot.enableSchedule() 92 | * robot.disableSchedule() 93 | * robot.startCleaning() 94 | * robot.startSpotCleaning() 95 | * robot.stopCleaning() 96 | * robot.pauseCleaning() 97 | * robot.resumeCleaning() 98 | * robot.getPersistentMaps() 99 | * robot.getMapBoundaries() 100 | * robot.setMapBoundaries() 101 | * robot.startCleaningBoundary() 102 | * robot.sendToBase() 103 | * robot.findMe() 104 | 105 | ------------------------------------------------------- 106 | 107 | ### robot.getState([callback]) 108 | 109 | Returns the state object of the robot. Also updates all robot properties. 110 | 111 | * `callback` - `function(error, state)` 112 | * `error` ```null``` if no error occurred 113 | * `state` ```object``` 114 | * example: 115 | ```Javascript 116 | var state = { 117 | version: 1, 118 | reqId: '1', 119 | result: 'ok', 120 | error: 'ui_alert_invalid', 121 | data: {}, 122 | state: 1, 123 | action: 0, 124 | cleaning: {category: 2, mode: 1, modifier: 1, spotWidth: 0, spotHeight: 0}, 125 | details: { 126 | isCharging: false, 127 | isDocked: true, 128 | isScheduleEnabled: false, 129 | dockHasBeenSeen: false, 130 | charge: 98 131 | }, 132 | availableCommands: { 133 | start: true, 134 | stop: false, 135 | pause: false, 136 | resume: false, 137 | goToBase: false 138 | }, 139 | availableServices: { 140 | houseCleaning: 'basic-1', 141 | spotCleaning: 'basic-1', 142 | manualCleaning: 'basic-1', 143 | easyConnect: 'basic-1', 144 | schedule: 'basic-1' 145 | }, 146 | meta: {modelName: 'BotVacConnected', firmware: '2.0.0'}}; 147 | ``` 148 | 149 | ------------------------------------------------------- 150 | 151 | ### robot.getSchedule([detailed], [callback]) 152 | 153 | Returns the scheduling state of the robot. 154 | * `detailed` - `boolean` boolean, to return the full schedule object, not only it status 155 | * `callback` - `function(error, schedule)` 156 | * `error` ```null``` if no error occurred 157 | * `schedule` depend on `detailed` 158 | * ```boolean``` (when `detailed` is `undefined` or `false`) true if scheduling is enabled 159 | * ```object``` (when `detailed` is `true`) full schedule description object 160 | * example: 161 | ```Javascript 162 | var schedule = { 163 | type:1, 164 | enabled:true, 165 | events:[ 166 | { 167 | day:1, 168 | startTime:"08:30" 169 | }, 170 | { 171 | day:2, 172 | startTime:"08:30" 173 | }, 174 | { 175 | day:3, 176 | startTime:"08:30" 177 | }, 178 | { 179 | day:4, 180 | startTime:"08:30" 181 | }, 182 | { 183 | day:5, 184 | startTime:"08:30" 185 | }, 186 | { 187 | day:6, 188 | startTime:"11:30" 189 | }, 190 | { 191 | day:0, 192 | startTime:"11:30" 193 | } 194 | ] 195 | } 196 | ``` 197 | 198 | ------------------------------------------------------- 199 | 200 | ### robot.enableSchedule([callback]) 201 | 202 | Enables scheduling. 203 | 204 | * `callback` - `function(error, result)` 205 | * `error` null if no error occurred 206 | * `result` string - 'ok' if scheduling got enabled 207 | 208 | ------------------------------------------------------- 209 | 210 | ### robot.disableSchedule([callback]) 211 | 212 | Disables scheduling. 213 | 214 | * `callback` - `function(error, result)` 215 | * `error` null if no error occurred 216 | * `result` string - 'ok' if scheduling got disabled 217 | 218 | ------------------------------------------------------- 219 | 220 | ### robot.startCleaning([eco], [navigationMode], [noGoLines], [callback]) 221 | 222 | Start cleaning. 223 | 224 | * `eco` boolean - clean in eco mode 225 | * `navigationMode` number - 1: normal, 2: extra care (new models only) 226 | * `eco` boolean - clean with enabled nogo lines 227 | * `callback` - `function(error, result)` 228 | * `error` null if no error occurred 229 | * `result` string - 'ok' if cleaning could be started 230 | 231 | ------------------------------------------------------- 232 | 233 | ### robot.startSpotCleaning([eco], [width], [height], [repeat], [navigationMode], [callback]) 234 | 235 | Start spot cleaning. 236 | 237 | * `eco` boolean - clean in eco mode 238 | * `width` number - spot width in cm (min 100cm) 239 | * `height` number - spot height in cm (min 100cm) 240 | * `repeat` boolean - clean spot two times 241 | * `navigationMode` number - 1: normal, 2: extra care (new models only) 242 | * `callback` - `function(error, result)` 243 | * `error` null if no error occurred 244 | * `result` string - 'ok' if spot cleaning could be started 245 | 246 | ------------------------------------------------------- 247 | 248 | ### robot.stopCleaning([callback]) 249 | 250 | Stop cleaning. 251 | 252 | * `callback` - `function(error, result)` 253 | * `error` null if no error occurred 254 | * `result` string - 'ok' if cleaning could be stopped 255 | 256 | ------------------------------------------------------- 257 | 258 | ### robot.pauseCleaning([callback]) 259 | 260 | Pause cleaning. 261 | 262 | * `callback` - `function(error, result)` 263 | * `error` null if no error occurred 264 | * `result` string - 'ok' if cleaning could be paused 265 | 266 | ------------------------------------------------------- 267 | 268 | ### robot.resumeCleaning([callback]) 269 | 270 | Resume cleaning. 271 | 272 | * `callback` - `function(error, result)` 273 | * `error` null if no error occurred 274 | * `result` string - 'ok' if cleaning could be resumed 275 | 276 | ------------------------------------------------------- 277 | 278 | ### robot.getPersistentMaps([callback]) 279 | 280 | Returns the persistent maps of the robot 281 | 282 | * `callback` - `function(error, schedule)` 283 | * `error` null if no error occurred 284 | * `maps` Maps[] - array of maps 285 | 286 | ------------------------------------------------------- 287 | 288 | ### robot.getMapBoundaries(mapId, [callback]) 289 | 290 | Returns the boundaries of a map 291 | * `mapId` string - a Map id for which to get the boundaries 292 | * `callback` - `function(error, schedule)` 293 | * `error` null if no error occurred 294 | * `boundaries` Boundary[] - array of boundaries 295 | 296 | ------------------------------------------------------- 297 | 298 | ### robot.setMapBoundaries(mapId, [callback]) 299 | 300 | Sets boundaries for a map 301 | * `mapId` string - a Map id for which to get the boundaries 302 | * `boundaries` Boundary[] - array of boundaries 303 | * `callback` - `function(error, schedule)` 304 | * `error` null if no error occurred 305 | * `boundaries` Boundary[] - array of boundaries 306 | 307 | ------------------------------------------------------- 308 | 309 | ### robot.startCleaningBoundary([eco], [extraCare], [boundaryId], [callback]) 310 | 311 | Start cleaning with boundaries 312 | 313 | * `eco` boolean - clean in eco mode 314 | * `extraCare` boolean - clean in extra care (new models only) 315 | * `boundaryId` string - a boundary id (zone) to clean 316 | * `callback` - `function(error, result)` 317 | * `error` null if no error occurred 318 | * `result` string - 'ok' if cleaning could be started 319 | 320 | ------------------------------------------------------- 321 | 322 | ### robot.sendToBase([callback]) 323 | 324 | Send robot to base. 325 | 326 | * `callback` - `function(error, result)` 327 | * `error` null if no error occurred 328 | * `result` string - 'ok' if robot could be sent to base 329 | 330 | ------------------------------------------------------- 331 | 332 | ### robot.findMe([callback]) 333 | 334 | Locate the robot by emitting a sound and light 335 | 336 | * `callback` - `function(error, result)` 337 | * `error` null if no error occurred 338 | * `result` string - 'ok' if robot could be located 339 | 340 | ## Changelog 341 | ### 0.4.3 342 | * (Pmant) update dependencies 343 | ### 0.4.2 344 | * (PeterVoronov) add optional detailed parameter to robot.getSchedule 345 | * (naofireblade) add isBinFull property 346 | * (naofireblade) prevent request from catching exceptions in callback 347 | ### 0.4.1 348 | * (jbtibor) update dependencies 349 | ### 0.4.0 350 | * (naofireblade) add findMe 351 | ### 0.3.0 352 | * (az0uz) add persistent maps and boundaries 353 | ### 0.2.0 354 | * (koush) http transport changes and updates 355 | ### 0.1.5 356 | * (naofireblade) add support for new parameter navigationMode (newer models) 357 | ### 0.1.6 358 | * (naofireblade) add support for new parameter noGoLines (newer models) 359 | * (naofireblade) changed to keep cleaning parameters in sync with neato app 360 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var api = require(__dirname + '/lib/api'); 2 | var client = require(__dirname + '/lib/client'); 3 | 4 | module.exports.Client = client; 5 | module.exports.api = api; -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | var axios = require('axios'); 2 | 3 | function request(url, payload, method, headers, callback) { 4 | if (!url || url === '') { 5 | if (typeof callback === 'function') callback('no url specified'); 6 | return; 7 | } 8 | 9 | var options = { 10 | method: method === 'GET' ? 'GET' : 'POST', 11 | url: url, 12 | headers: { 13 | 'Accept': 'application/vnd.neato.nucleo.v1' 14 | } 15 | }; 16 | 17 | if (options.method === 'POST') { 18 | options.data = payload; 19 | } 20 | 21 | if (typeof headers === 'object') { 22 | for (var header in headers) { 23 | if (headers.hasOwnProperty(header)) { 24 | options.headers[header] = headers[header]; 25 | } 26 | } 27 | } 28 | 29 | let res, err; 30 | 31 | axios(options) 32 | .then(function (response) { 33 | res = response.data; 34 | }) 35 | .catch(function (error) { 36 | err = error; 37 | }) 38 | .finally(function () { 39 | // Callback needs to be called in finally block, see: https://github.com/Pmant/node-botvac/issues/15 40 | if (typeof callback === 'function') callback(err, res); 41 | }); 42 | } 43 | 44 | exports.request = request; 45 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | var api = require(__dirname + '/api'); 2 | var robot = require(__dirname + '/robot'); 3 | 4 | /** 5 | * Initializes a botvac client with an authorization token. 6 | * @param {String} t - Token used to authorize calls to Neato API. 7 | * @param {String} [tokenType] - Token type, optional. Valid values: OAuth, token must be an OAuth access token, see https://developers.neatorobotics.com/guides/oauth-flow. Any other token type value requires a session token issued by a login with email and password. 8 | */ 9 | function Client(t, tokenType) { 10 | this._baseUrl = 'https://beehive.neatocloud.com'; 11 | this._token = t; 12 | switch (tokenType) { 13 | case 'OAuth': 14 | this._tokenType = 'Bearer '; 15 | break; 16 | default: 17 | this._tokenType = 'Token token='; 18 | break; 19 | } 20 | } 21 | 22 | Client.prototype.authorize = function (email, password, force, callback) { 23 | if (!this._token || force) { 24 | api.request(this._baseUrl + '/sessions', {email: email, password: password}, 'POST', null, (function (error, body) { 25 | if (!error && body.access_token) { 26 | this._token = body.access_token; 27 | callback(); 28 | } else { 29 | if (typeof callback === 'function') { 30 | if (error) { 31 | callback(error); 32 | } else if (body.message) { 33 | callback(body.message); 34 | } else { 35 | callback('unkown error'); 36 | } 37 | } 38 | } 39 | }).bind(this)); 40 | } else { 41 | callback(); 42 | } 43 | 44 | }; 45 | 46 | Client.prototype.getRobots = function (callback) { 47 | if (this._token) { 48 | api.request(this._baseUrl + '/users/me/robots', null, 'GET', {Authorization: this._tokenType + this._token}, (function (error, body) { 49 | if (!error && body) { 50 | var robots = []; 51 | for (var i = 0; i < body.length; i++) { 52 | robots.push(new robot(body[i].name, body[i].serial, body[i].secret_key, this._tokenType + this._token)); 53 | } 54 | callback(null, robots); 55 | } else { 56 | if (typeof callback === 'function') { 57 | if (error) { 58 | callback(error); 59 | } else if (body.message) { 60 | callback(body.message); 61 | } else { 62 | callback('unkown error'); 63 | } 64 | } 65 | } 66 | }).bind(this)); 67 | } else { 68 | if (typeof callback === 'function') { 69 | callback('not authorized'); 70 | } 71 | } 72 | }; 73 | 74 | Client.prototype.reauthorize = function (email, password, callback) { 75 | Client.authorize(email, password, true, callback); 76 | }; 77 | 78 | module.exports = Client; 79 | -------------------------------------------------------------------------------- /lib/robot.js: -------------------------------------------------------------------------------- 1 | var api = require(__dirname + '/api'); 2 | var crypto = require('crypto'); 3 | 4 | function Robot(name, serial, secret, token) { 5 | this._nucleoBaseUrl = 'https://nucleo.neatocloud.com:4443/vendors/neato/robots/'; 6 | this._beehiveBaseUrl = 'https://beehive.neatocloud.com/users/me/robots/'; 7 | this.name = name; 8 | this._serial = serial; 9 | this._secret = secret; 10 | this._token = token; 11 | 12 | //updated when getState() is called 13 | this.availableServices = null; 14 | this.isBinFull = null; 15 | this.isCharging = null; 16 | this.isDocked = null; 17 | this.isScheduleEnabled = null; 18 | this.dockHasBeenSeen = null; 19 | this.charge = null; 20 | this.canStart = null; 21 | this.canStop = null; 22 | this.canPause = null; 23 | this.canResume = null; 24 | this.canGoToBase = null; 25 | this.eco = null; 26 | this.noGoLines = null; 27 | this.navigationMode = null; 28 | this.spotWidth = null; 29 | this.spotHeight = null; 30 | this.spotRepeat = null; 31 | this.cleaningBoundaryId = null; 32 | } 33 | 34 | Robot.prototype.getState = function getState(callback) { 35 | doAction(this, 'getRobotState', null, (function (error, result) { 36 | if (typeof callback === 'function') { 37 | if (result === undefined) { 38 | if (!error) { 39 | error = 'no result'; 40 | } 41 | callback(error, null); 42 | } else if (result && 'message' in result) { 43 | callback(result.message, result); 44 | } else { 45 | this.availableServices = result.availableServices; 46 | this.isBinFull = result.alert == "dustbin_full"; 47 | this.isCharging = result.details.isCharging; 48 | this.isDocked = result.details.isDocked; 49 | this.isScheduleEnabled = result.details.isScheduleEnabled; 50 | this.dockHasBeenSeen = result.details.dockHasBeenSeen; 51 | this.charge = result.details.charge; 52 | this.canStart = result.availableCommands.start; 53 | this.canStop = result.availableCommands.stop; 54 | this.canPause = result.availableCommands.pause; 55 | this.canResume = result.availableCommands.resume; 56 | this.canGoToBase = result.availableCommands.goToBase; 57 | 58 | // Read cleaning parameters from last run 59 | this.eco = result.cleaning.mode === 1; 60 | // 4: house cleaning with noGoLines 61 | if (result.cleaning.category === 4) { 62 | this.noGoLines = true; 63 | } 64 | // 2: house cleaning without noGoLines 65 | else if (result.cleaning.category === 2) { 66 | this.noGoLines = false; 67 | } 68 | // 1+3: spot+manual cleaning. Set nogolines to default = false only if not set already 69 | else if (this.noGoLines === null) { 70 | this.noGoLines = false; 71 | } 72 | this.navigationMode = result.cleaning.navigationMode; 73 | this.spotWidth = result.cleaning.spotWidth; 74 | this.spotHeight = result.cleaning.spotHeight; 75 | this.spotRepeat = result.cleaning.modifier === 2; 76 | this.cleaningBoundaryId = result.cleaning.boundaryId; 77 | callback(error, result); 78 | } 79 | } 80 | }).bind(this)); 81 | }; 82 | 83 | Robot.prototype.getSchedule = function getSchedule(detailed, callback) { 84 | if (typeof detailed === 'function') { 85 | callback = detailed; 86 | detailed = false; 87 | } 88 | doAction(this, 'getSchedule', null, function (error, result) { 89 | if (typeof callback === 'function') { 90 | if (error) { 91 | callback(error, result); 92 | } else if (result && 'data' in result && (detailed !== undefined) && detailed ) { 93 | callback(null, result.data); 94 | } else if (result && 'data' in result && 'enabled' in result.data) { 95 | callback(null, result.data.enabled); 96 | } else { 97 | callback(result && 'message' in result ? result.message : 'failed', result); 98 | } 99 | } 100 | }); 101 | }; 102 | 103 | 104 | let timeValidator = new RegExp(/^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$/); 105 | 106 | Robot.prototype.setSchedule = function setSchedule(schedule, callback) { 107 | let error = ''; 108 | let debug = ''; 109 | if ((schedule === undefined) || (schedule === null) || (typeof schedule !== 'object') || (!Object.keys(schedule).length)) { 110 | error = 'Wrong type of schedule object!'; 111 | } 112 | else if (!this.availableServices.hasOwnProperty('schedule')) { 113 | error = 'Schedule is not suported by robot!'; 114 | } 115 | else if (!schedule.hasOwnProperty('type') || (! Number.isInteger(schedule.type)) || (schedule.type !== 1)) { 116 | error = 'Mandatory property type is missed or mailformed!'; 117 | } 118 | else if (!schedule.hasOwnProperty('events') || (!Array.isArray(schedule.events)) || (schedule.events.length > 7)) { 119 | error = 'Mandatory property events is missed or mailformed!'; 120 | } 121 | else if (! schedule.events.every(function scheduleValidate (event) { 122 | let eventLength = Object.keys(event).length; 123 | //console.log() doesn't work under ioBroker, thats why such construction is used 124 | 125 | //check maximum possible fields amount per type of schedule 126 | debug += "\n" + '1 ' + JSON.stringify((((this.availableServices.schedule === 'minimal-1') && (eventLength > 2)) || ((this.availableServices.schedule === 'basic-1') && (eventLength > 3)) 127 | || ((this.availableServices.schedule === 'basic-2') && (eventLength > 4)))) ; 128 | 129 | if (((this.availableServices.schedule === 'minimal-1') && (eventLength > 2)) || ((this.availableServices.schedule === 'basic-1') && (eventLength > 3)) 130 | || ((this.availableServices.schedule === 'basic-2') && (eventLength > 4))) return false; 131 | 132 | 133 | //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 134 | debug += "\n" + ' 2 ' + JSON.stringify(((! event.hasOwnProperty('day')) || (! Number.isInteger(event.day)) || ( event.day < 0 ) || ( event.day > 6))); 135 | 136 | if ((! event.hasOwnProperty('day')) || (! Number.isInteger(event.day)) || ( event.day < 0 ) || ( event.day > 6)) return false; 137 | 138 | 139 | //property startTime is mandatory and has to be in 'HH:MM' format 140 | debug += "\n" + ' 3 ' + JSON.stringify(((! event.hasOwnProperty('startTime')) || ( typeof event.startTime !== 'string') || ( ! event.startTime ) || (! timeValidator.test(event.startTime)))); 141 | 142 | if ((! event.hasOwnProperty('startTime')) || ( typeof event.startTime !== 'string') || ( ! event.startTime ) || (! timeValidator.test(event.startTime))) return false; 143 | 144 | 145 | //property mode is not available on 'minimal-1' schedule type 146 | debug += "\n" + ' 4 ' + JSON.stringify(((this.availableServices.schedule === 'minimal-1') && (event.hasOwnProperty('mode')))); 147 | 148 | if ((this.availableServices.schedule === 'minimal-1') && (event.hasOwnProperty('mode'))) return false; 149 | 150 | 151 | //but is required for basic-1 and basic-2 152 | debug += "\n" + ' 5 ' + JSON.stringify((((this.availableServices.schedule === 'basic-1') || (this.availableServices.schedule === 'basic-1')) && (!event.hasOwnProperty('mode')))); 153 | 154 | if (((this.availableServices.schedule === 'basic-1') || (this.availableServices.schedule === 'basic-1')) && (!event.hasOwnProperty('mode'))) return false; 155 | 156 | //mode - the cleaning mode. 1 Eco 2 Normal. 157 | debug += "\n" + ' 6 ' + JSON.stringify(((event.hasOwnProperty('mode')) && ((! Number.isInteger(event.mode)) || ( event.mode < 1 ) || ( event.mode > 2)))); 158 | 159 | if ((event.hasOwnProperty('mode')) && ((! Number.isInteger(event.mode)) || ( event.mode < 1 ) || ( event.mode > 2))) return false; 160 | 161 | 162 | //initially setting boundaries is not supported 163 | 164 | debug += "\n" + ' 7 ' + JSON.stringify((((this.availableServices.schedule === 'minimal-1') || (this.availableServices.schedule === 'basic-1')) && (event.hasOwnProperty('boundaryId')))); 165 | 166 | if (((this.availableServices.schedule === 'minimal-1') || (this.availableServices.schedule === 'basic-1')) && (event.hasOwnProperty('boundaryId'))) return false; 167 | 168 | // to process boundaries from ioBroker, as it not editable, and will be reused from original 169 | //debug += "\n" + ' 8 ' + JSON.stringify((event.hasOwnProperty('boundaryId') && event.boundaryId )); 170 | //if (event.hasOwnProperty('boundaryId') && event.boundaryId ) return false; 171 | 172 | return true; 173 | }, this) 174 | ) { 175 | error = 'Wrong structure of the schedule! Please check!' + ' ' + debug; 176 | } 177 | if (error) { 178 | if (typeof callback === 'function') { 179 | callback(error, schedule); 180 | } 181 | return; 182 | } 183 | doAction(this, 'setSchedule', schedule, function (error, result) { 184 | if (typeof callback === 'function') { 185 | if (error) { 186 | callback(error, result); 187 | } else if (result && 'data' in result ) { 188 | callback(null, result.data); 189 | } else { 190 | callback(result && 'message' in result ? result.message : 'failed', result); 191 | } 192 | } 193 | }); 194 | }; 195 | 196 | 197 | Robot.prototype.enableSchedule = function enableSchedule(callback) { 198 | doAction(this, 'enableSchedule', null, function (error, result) { 199 | if (typeof callback === 'function') { 200 | if (error) { 201 | callback(error, result); 202 | } else if (result && 'result' in result && result.result === 'ok') { 203 | callback(null, result.result); 204 | } else { 205 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 206 | } 207 | } 208 | }); 209 | }; 210 | 211 | Robot.prototype.disableSchedule = function disableSchedule(callback) { 212 | doAction(this, 'disableSchedule', null, function (error, result) { 213 | if (typeof callback === 'function') { 214 | if (error) { 215 | callback(error, result); 216 | } else if (result && 'result' in result && result.result === 'ok') { 217 | callback(null, result.result); 218 | } else { 219 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 220 | } 221 | } 222 | }); 223 | }; 224 | 225 | 226 | 227 | Robot.prototype.sendToBase = function sendToBase(callback) { 228 | doAction(this, 'sendToBase', null, function (error, result) { 229 | if (typeof callback === 'function') { 230 | if (error) { 231 | callback(error, result); 232 | } else if (result && 'result' in result && result.result === 'ok') { 233 | callback(null, result.result); 234 | } else { 235 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 236 | } 237 | } 238 | }); 239 | }; 240 | 241 | Robot.prototype.stopCleaning = function stopCleaning(callback) { 242 | doAction(this, 'stopCleaning', null, function (error, result) { 243 | if (typeof callback === 'function') { 244 | if (error) { 245 | callback(error, result); 246 | } else if (result && 'result' in result && result.result === 'ok') { 247 | callback(null, result.result); 248 | } else { 249 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 250 | } 251 | } 252 | }); 253 | }; 254 | 255 | Robot.prototype.pauseCleaning = function pauseCleaning(callback) { 256 | doAction(this, 'pauseCleaning', null, function (error, result) { 257 | if (typeof callback === 'function') { 258 | if (error) { 259 | callback(error, result); 260 | } else if (result && 'result' in result && result.result === 'ok') { 261 | callback(null, result.result); 262 | } else { 263 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 264 | } 265 | } 266 | }); 267 | }; 268 | 269 | Robot.prototype.resumeCleaning = function resumeCleaning(callback) { 270 | doAction(this, 'resumeCleaning', null, function (error, result) { 271 | if (typeof callback === 'function') { 272 | if (error) { 273 | callback(error, result); 274 | } else if (result && 'result' in result && result.result === 'ok') { 275 | callback(null, result.result); 276 | } else { 277 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 278 | } 279 | } 280 | }); 281 | }; 282 | 283 | Robot.prototype.startSpotCleaning = function startSpotCleaning(eco, width, height, repeat, navigationMode, callback) { 284 | if (typeof eco === 'function') { 285 | callback = eco; 286 | eco = this.eco; 287 | width = this.spotWidth; 288 | height = this.spotHeight; 289 | repeat = this.spotRepeat; 290 | navigationMode = this.navigationMode; 291 | } else if (typeof width === 'function') { 292 | callback = width; 293 | width = this.spotWidth; 294 | height = this.spotHeight; 295 | repeat = this.spotRepeat; 296 | navigationMode = this.navigationMode; 297 | } else if (typeof height === 'function') { 298 | callback = height; 299 | height = this.spotHeight; 300 | repeat = this.spotRepeat; 301 | navigationMode = this.navigationMode; 302 | } else if (typeof repeat === 'function') { 303 | callback = repeat; 304 | repeat = this.spotRepeat; 305 | navigationMode = this.navigationMode; 306 | } else if (typeof navigationMode === 'function') { 307 | callback = navigationMode; 308 | navigationMode = this.navigationMode; 309 | } 310 | 311 | if (typeof width !== 'number' || width < 100) { 312 | width = this.width; 313 | } 314 | if (typeof height !== 'number' || height < 100) { 315 | height = this.height; 316 | } 317 | if (typeof navigationMode !== 'number' || navigationMode < 1 || navigationMode > 2) { 318 | navigationMode = this.navigationMode; 319 | } 320 | var params = { 321 | category: 3, //1: manual, 2: house, 3: spot, 4: house with enabled nogolines 322 | mode: eco ? 1 : 2, //1: eco, 2: turbo 323 | modifier: repeat ? 2 : 1, //spot: clean spot 1 or 2 times 324 | navigationMode: navigationMode, //1: normal, 2: extra care 325 | spotWidth: width, 326 | spotHeight: height 327 | }; 328 | doAction(this, 'startCleaning', params, function (error, result) { 329 | if (typeof callback === 'function') { 330 | if (error) { 331 | callback(error, result); 332 | } else if (result && 'result' in result && result.result === 'ok') { 333 | callback(null, result.result); 334 | } else { 335 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 336 | } 337 | } 338 | }); 339 | }; 340 | 341 | Robot.prototype.startManualCleaning = function startSpotCleaning(eco, navigationMode, callback) { 342 | if (typeof eco === 'function') { 343 | callback = eco; 344 | eco = this.eco; 345 | navigationMode = this.navigationMode; 346 | } else if (typeof navigationMode === 'function') { 347 | callback = navigationMode; 348 | navigationMode = this.navigationMode; 349 | } 350 | 351 | var params = { 352 | category: 1, //1: manual, 2: house, 3: spot, 4: house with enabled nogolines 353 | mode: eco ? 1 : 2, //1: eco, 2: turbo 354 | modifier: 1, //spot: clean spot 1 or 2 times 355 | navigationMode: navigationMode //1: normal, 2: extra care 356 | }; 357 | doAction(this, 'startCleaning', params, function (error, result) { 358 | if (typeof callback === 'function') { 359 | if (error) { 360 | callback(error, result); 361 | } else if (result && 'result' in result && result.result === 'ok') { 362 | callback(null, result.result); 363 | } else { 364 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 365 | } 366 | } 367 | }); 368 | }; 369 | 370 | Robot.prototype.startCleaning = function startSpotCleaning(eco, navigationMode, noGoLines, callback) { 371 | if (typeof eco === 'function') { 372 | callback = eco; 373 | eco = this.eco; 374 | navigationMode = this.navigationMode; 375 | noGoLines = this.noGoLines; 376 | } else if (typeof navigationMode === 'function') { 377 | callback = navigationMode; 378 | navigationMode = this.navigationMode; 379 | noGoLines = this.noGoLines; 380 | } else if (typeof noGoLines === 'function') { 381 | callback = noGoLines; 382 | noGoLines = this.noGoLines; 383 | } 384 | 385 | var params = { 386 | category: noGoLines ? 4 : 2, //1: manual, 2: house, 3: spot, 4: house with enabled nogolines and/or boundaries 387 | mode: eco ? 1 : 2, //1: eco, 2: turbo 388 | modifier: 1, //spot: clean spot 1 or 2 times 389 | navigationMode: navigationMode //1: normal, 2: extra care 390 | }; 391 | doAction(this, 'startCleaning', params, function (error, result) { 392 | if (typeof callback === 'function') { 393 | if (error) { 394 | callback(error, result); 395 | } else if (result && 'result' in result && result.result === 'ok') { 396 | callback(null, result.result); 397 | } else { 398 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 399 | } 400 | } 401 | }); 402 | }; 403 | 404 | /** 405 | * A default action callback 406 | * @callback defaultActionCallback 407 | * @param {string} [error] - An error message if an error occur 408 | * @param {string|*} maps - "ok" on success or the request result if an error occur 409 | */ 410 | 411 | /** 412 | * Start cleaning a specific boundary (of type polygone) 413 | * 414 | * @param {bool} eco - enable eco cleaning or do a turbo clean 415 | * @param {bool} extraCare - enable extra care 416 | * @param {string} boundaryId - A {@link Boundary} id to clean (uuid-v4) 417 | * @param {defaultActionCallback} callback - a callback called when executing the action or an error 418 | */ 419 | Robot.prototype.startCleaningBoundary = function startSpotCleaning(eco, extraCare, boundaryId, callback) { 420 | if (typeof eco === 'function') { 421 | callback = eco; 422 | eco = this.eco; 423 | navigationMode = this.navigationMode; 424 | noGoLines = this.noGoLines; 425 | } else if (typeof navigationMode === 'function') { 426 | callback = navigationMode; 427 | navigationMode = this.navigationMode; 428 | noGoLines = this.noGoLines; 429 | } else if (typeof noGoLines === 'function') { 430 | callback = noGoLines; 431 | noGoLines = this.noGoLines; 432 | } 433 | 434 | var params = { 435 | boundaryId: boundaryId, // Boundary to clean 436 | category: 4, // 4: house with enabled nogolines and/or boundaries 437 | mode: eco ? 1 : 2, //1: eco, 2: turbo 438 | navigationMode: extraCare ? 2 : 1 //1: normal, 2: extra care 439 | }; 440 | doAction(this, 'startCleaning', params, function (error, result) { 441 | if (typeof callback === 'function') { 442 | if (error) { 443 | callback(error, result); 444 | } else if (result && 'result' in result && result.result === 'ok') { 445 | callback(null, result.result); 446 | } else { 447 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 448 | } 449 | } 450 | }); 451 | }; 452 | 453 | /** 454 | * A Map 455 | * @typedef {Object} Map 456 | * @property {string} id - The title 457 | * @property {string} name - The artist 458 | * @property {string} raw_floor_map_url - url pointing to a png image of the raw floor map 459 | * @property {string} url - url pointing to a png image of the floor map 460 | * @property {number} url_valid_for_seconds - number of seconds the map urls are valid 461 | */ 462 | 463 | /** 464 | * Callback for persistent maps. 465 | * 466 | * @callback getPersistentMapsCallback 467 | * @param {string} [error] - An error message if an error occur 468 | * @param {Map[]} maps - The list of {@link Map} for this robot 469 | */ 470 | 471 | /** 472 | * Add two numbers together, then pass the results to a callback function. 473 | * 474 | * @param {getPersistentMapsCallback} callback - A callback called on receiving the maps or an error 475 | */ 476 | Robot.prototype.getPersistentMaps = function getPersistentMaps(callback) { 477 | robotRequest(this, 'beehive', 'GET', '/persistent_maps', null, callback); 478 | } 479 | 480 | /** 481 | * A Boundary (zone or no go line) 482 | * 483 | * @typedef {Object} Boundary 484 | * @property {string} color - color hex code for a type polygone or '#000000' for a type polyline 485 | * @property {bool} enabled - always true, unknown usage 486 | * @property {string} id - boundary id (uuid-v4) 487 | * @property {string} name - polygone name or empty string for a type polyline 488 | * @property {number[]} [relevancy] - array of 2 number, center of a type polygone 489 | * @property {string} type - either polyline (for a no go lines) or polygon (for a zone) 490 | * @property {number[][]} vertices - array of array of two points, coordinates of the points 491 | */ 492 | 493 | /** 494 | * Callback for map boundaries. 495 | * 496 | * @callback getMapBoundariesCallback 497 | * @param {error} error - An integer. 498 | * @param {Boundary[]|*} - An array of {@link Boundary} for the specified {@link Map} or the request result if an error occur 499 | */ 500 | 501 | /** 502 | * Add two numbers together, then pass the results to a callback function. 503 | * 504 | * @param {string} mapId - An id from a {@link Map} to request its list of {@link Boundary} 505 | * @param {getMapBoundariesCallback} callback - A callback called on receiving the boundaries or an error 506 | */ 507 | Robot.prototype.getMapBoundaries = function getMapBoundaries(mapId, callback) { 508 | doAction(this, 'getMapBoundaries', { mapId: mapId }, function (error, result) { 509 | if (typeof callback === 'function') { 510 | if (error) { 511 | callback(error, result); 512 | } else if (result && 'data' in result && 'boundaries' in result.data) { 513 | callback(null, { mapId: mapId, boundaries: result.data.boundaries }); 514 | } else { 515 | callback(result && 'message' in result ? result.message : 'failed', result); 516 | } 517 | } 518 | }); 519 | } 520 | 521 | /** 522 | * Callback when setting map boundaries. 523 | * 524 | * @callback setMapBoundariesCallback 525 | * @param {error} error - An integer. 526 | * @param {Boundary[]} - An array of {@link Boundary} for the specified {@link Map} 527 | */ 528 | 529 | /** 530 | * Add two numbers together, then pass the results to a callback function. 531 | * 532 | * @param {string} mapId - An id from a {@link Map} to set its list of {@link Boundary} 533 | * @param {Boundary[]} boundaries - List of all new {@link Boundary} for the given {@link Map} 534 | * @param {setMapBoundariesCallback} callback - A callback called on receiving the boundaries or an error 535 | */ 536 | Robot.prototype.setMapBoundaries = function setMapBoundaries(mapId, boundaries, callback) { 537 | doAction(this, 'setMapBoundaries', { mapId: mapId, boundaries: boundaries }, function (error, result) { 538 | if (typeof callback === 'function') { 539 | if (error) { 540 | callback(error, result); 541 | } else if (result && 'data' in result && 'boundaries' in result.data) { 542 | callback(null, { mapId: mapId, boundaries: result.data.boundaries }); 543 | } else { 544 | callback(result && 'message' in result ? result.message : 'failed', result); 545 | } 546 | } 547 | }); 548 | } 549 | 550 | /** 551 | * A findMe callback 552 | * @callback findMeCallback 553 | * @param {string} [error] - An error message if an error occur 554 | * @param {string} result - "ok" on success or the request result if an error occur 555 | */ 556 | 557 | /** 558 | * Let the robot emit a sound and light to find him 559 | * 560 | * @param {findMeCallback} callback - a callback called when executing the action or an error 561 | */ 562 | Robot.prototype.findMe = function findMe(callback) { 563 | var params = {}; 564 | doAction(this, 'findMe', params, function (error, result) { 565 | if (typeof callback === 'function') { 566 | if (error) { 567 | callback(error, result); 568 | } else if (result && 'result' in result && result.result === 'ok') { 569 | callback(null, result.result); 570 | } else { 571 | callback(result && 'message' in result ? result.message : 'failed', result && 'result' in result ? result.result : result); 572 | } 573 | } 574 | }); 575 | }; 576 | 577 | var robotMessagesRequestId = 1; 578 | function doAction(robot, command, params, callback) { 579 | var payload = { 580 | reqId: robotMessagesRequestId++, 581 | cmd: command 582 | }; 583 | if (params) { 584 | payload.params = params; 585 | } 586 | robotRequest(robot, 'nucleo', 'POST', '/messages', payload, callback); 587 | } 588 | 589 | function robotRequest(robot, service, type, endpoint, payload, callback) { 590 | if (robot._serial && robot._secret) { 591 | payload = JSON.stringify(payload); 592 | var date = new Date().toUTCString(); 593 | var data = [robot._serial.toLowerCase(), date, payload].join("\n"); 594 | var headers = { 595 | Date: date 596 | }; 597 | var url; 598 | if (service === 'nucleo') { 599 | var hmac = crypto.createHmac('sha256', robot._secret).update(data).digest('hex'); 600 | headers.Authorization = 'NEATOAPP ' + hmac; 601 | url = robot._nucleoBaseUrl + robot._serial + endpoint 602 | } else if (service === 'beehive') { 603 | headers.Authorization = robot._token; 604 | url = robot._beehiveBaseUrl + robot._serial + endpoint 605 | } else { 606 | callback('Service' + service + 'unknown'); 607 | } 608 | api.request(url, payload, type, headers, function (error, body) { 609 | if (typeof callback === 'function') { 610 | callback(error, body); 611 | } 612 | }); 613 | } else { 614 | if (typeof callback === 'function') { 615 | callback('no serial or secret'); 616 | } 617 | } 618 | } 619 | 620 | module.exports = Robot; 621 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-botvac", 3 | "version": "0.4.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "axios": { 8 | "version": "0.21.4", 9 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", 10 | "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", 11 | "requires": { 12 | "follow-redirects": "^1.14.0" 13 | } 14 | }, 15 | "follow-redirects": { 16 | "version": "1.14.8", 17 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", 18 | "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-botvac", 3 | "version": "0.4.3", 4 | "description": "Neato Botvac API", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Pmant/node-botvac.git" 12 | }, 13 | "keywords": [ 14 | "neato", 15 | "botvac" 16 | ], 17 | "author": "Pmant", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Pmant/node-botvac/issues" 21 | }, 22 | "homepage": "https://github.com/Pmant/node-botvac#readme", 23 | "dependencies": { 24 | "axios": "^0.21.4" 25 | } 26 | } 27 | --------------------------------------------------------------------------------