├── .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 |
--------------------------------------------------------------------------------