homebridge-adt-pulse 2.2.0 → 3.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +13 -13
- package/package.json +39 -17
- package/src/index.ts +18 -0
- package/src/lib/accessory.ts +405 -0
- package/src/lib/api.ts +3483 -0
- package/src/lib/detect.ts +728 -0
- package/src/lib/platform.ts +890 -0
- package/src/lib/regex.ts +167 -0
- package/src/lib/schema.ts +34 -0
- package/src/lib/utility.ts +933 -0
- package/src/scripts/repl.ts +300 -0
- package/src/scripts/test-api.ts +278 -0
- package/src/types/constant.d.ts +308 -0
- package/src/types/index.d.ts +1472 -0
- package/src/types/shared.d.ts +517 -0
- package/api-test.js +0 -280
- package/api.js +0 -878
- package/index.js +0 -1312
package/api.js
DELETED
|
@@ -1,878 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ADT Pulse.
|
|
3
|
-
*
|
|
4
|
-
* A JavaScript library / Node module for ADT Pulse.
|
|
5
|
-
*
|
|
6
|
-
* @author Kevin Hickey <kevin@kevinmhickey.com>
|
|
7
|
-
* @author Jacky Liang
|
|
8
|
-
*
|
|
9
|
-
* @since 1.0.0
|
|
10
|
-
*/
|
|
11
|
-
const cheerio = require('cheerio');
|
|
12
|
-
const hasInternet = require('internet-available');
|
|
13
|
-
const Q = require('q');
|
|
14
|
-
const request = require('request');
|
|
15
|
-
const _ = require('lodash');
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Browser session cookies.
|
|
19
|
-
*
|
|
20
|
-
* @since 1.0.0
|
|
21
|
-
*/
|
|
22
|
-
let jar;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Set country sub-domain.
|
|
26
|
-
*
|
|
27
|
-
* @since 1.0.0
|
|
28
|
-
*/
|
|
29
|
-
let countrySubDomain = '';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Track login, login statuses, portal versions.
|
|
33
|
-
*
|
|
34
|
-
* @since 1.0.0
|
|
35
|
-
*/
|
|
36
|
-
let authenticated = false;
|
|
37
|
-
let lastKnownVersion = '';
|
|
38
|
-
let lastKnownSiteId = '';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* ADT Pulse constructor.
|
|
42
|
-
*
|
|
43
|
-
* @param {object} options - The configuration.
|
|
44
|
-
*
|
|
45
|
-
* @constructor
|
|
46
|
-
*
|
|
47
|
-
* @since 1.0.0
|
|
48
|
-
*/
|
|
49
|
-
function Pulse(options) {
|
|
50
|
-
this.username = _.get(options, 'username', '');
|
|
51
|
-
this.password = _.get(options, 'password', '');
|
|
52
|
-
this.fingerprint = _.get(options, 'fingerprint', '');
|
|
53
|
-
this.overrideSensors = _.get(options, 'overrideSensors', []);
|
|
54
|
-
this.country = _.get(options, 'country', '');
|
|
55
|
-
this.debug = _.get(options, 'debug', false);
|
|
56
|
-
|
|
57
|
-
// Configure country sub-domain.
|
|
58
|
-
switch (this.country) {
|
|
59
|
-
case 'ca':
|
|
60
|
-
countrySubDomain = 'portal-ca';
|
|
61
|
-
break;
|
|
62
|
-
case 'us':
|
|
63
|
-
default:
|
|
64
|
-
countrySubDomain = 'portal';
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* ADT Pulse login.
|
|
71
|
-
*
|
|
72
|
-
* @returns {Q.Promise<object>}
|
|
73
|
-
*
|
|
74
|
-
* @since 1.0.0
|
|
75
|
-
*/
|
|
76
|
-
Pulse.prototype.login = function login() {
|
|
77
|
-
const deferred = Q.defer();
|
|
78
|
-
const that = this;
|
|
79
|
-
|
|
80
|
-
this.hasInternetWrapper(deferred, () => {
|
|
81
|
-
if (authenticated) {
|
|
82
|
-
deferred.resolve({
|
|
83
|
-
action: 'LOGIN',
|
|
84
|
-
success: true,
|
|
85
|
-
info: {
|
|
86
|
-
version: lastKnownVersion,
|
|
87
|
-
siteId: lastKnownSiteId,
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
} else {
|
|
91
|
-
this.consoleLogger('ADT Pulse: Logging in...', 'log');
|
|
92
|
-
|
|
93
|
-
// Request a new cookie session.
|
|
94
|
-
jar = request.jar();
|
|
95
|
-
|
|
96
|
-
request.get(
|
|
97
|
-
`https://${countrySubDomain}.adtpulse.com`,
|
|
98
|
-
this.generateRequestOptions(),
|
|
99
|
-
(error, response, body) => {
|
|
100
|
-
const regex = new RegExp(/(\/myhome\/)([0-9.-]+)(\/access\/signin\.jsp)/);
|
|
101
|
-
const responsePath = _.get(response, 'request.uri.path');
|
|
102
|
-
|
|
103
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${responsePath}`, 'log');
|
|
104
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${regex.test(responsePath)}`, 'log');
|
|
105
|
-
|
|
106
|
-
if (error || !regex.test(responsePath)) {
|
|
107
|
-
authenticated = false;
|
|
108
|
-
|
|
109
|
-
this.consoleLogger('ADT Pulse: Login failed.', 'error');
|
|
110
|
-
|
|
111
|
-
deferred.reject({
|
|
112
|
-
action: 'LOGIN',
|
|
113
|
-
success: false,
|
|
114
|
-
info: {
|
|
115
|
-
error,
|
|
116
|
-
message: this.getErrorMessage(body),
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
} else {
|
|
120
|
-
authenticated = false;
|
|
121
|
-
|
|
122
|
-
const version = responsePath.replace(regex, '$2');
|
|
123
|
-
|
|
124
|
-
// Saves last known version for reuse later.
|
|
125
|
-
lastKnownVersion = version;
|
|
126
|
-
|
|
127
|
-
this.consoleLogger(`ADT Pulse: Web portal version -> ${version}`, 'log');
|
|
128
|
-
|
|
129
|
-
request.post(
|
|
130
|
-
`https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/access/signin.jsp`,
|
|
131
|
-
this.generateRequestOptions({
|
|
132
|
-
followAllRedirects: true,
|
|
133
|
-
headers: {
|
|
134
|
-
Referer: `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/access/signin.jsp`,
|
|
135
|
-
},
|
|
136
|
-
form: {
|
|
137
|
-
usernameForm: that.username,
|
|
138
|
-
passwordForm: that.password,
|
|
139
|
-
fingerprint: that.fingerprint,
|
|
140
|
-
},
|
|
141
|
-
}),
|
|
142
|
-
(postError, postResponse, postBody) => {
|
|
143
|
-
const postRegex = new RegExp(/(\/myhome\/)([0-9.-]+)(\/summary\/summary\.jsp)/);
|
|
144
|
-
const postResponsePath = _.get(postResponse, 'request.uri.path');
|
|
145
|
-
|
|
146
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${postResponsePath}`, 'log');
|
|
147
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${postRegex.test(postResponsePath)}`, 'log');
|
|
148
|
-
|
|
149
|
-
if (postError || !postRegex.test(postResponsePath)) {
|
|
150
|
-
authenticated = false;
|
|
151
|
-
|
|
152
|
-
this.consoleLogger('ADT Pulse: Login failed.', 'error');
|
|
153
|
-
|
|
154
|
-
deferred.reject({
|
|
155
|
-
action: 'LOGIN',
|
|
156
|
-
success: false,
|
|
157
|
-
info: {
|
|
158
|
-
error: postError,
|
|
159
|
-
message: this.getErrorMessage(postBody),
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
} else {
|
|
163
|
-
authenticated = true;
|
|
164
|
-
|
|
165
|
-
const $ = cheerio.load(postBody);
|
|
166
|
-
const signoutLink = $('#p_signout1').attr('href');
|
|
167
|
-
const siteId = (signoutLink !== undefined) ? signoutLink.replace(/(.*)(networkid=)(.*)(&)(.*)/g, '$3') : undefined;
|
|
168
|
-
|
|
169
|
-
// Saves last known site ID for reuse later.
|
|
170
|
-
lastKnownSiteId = siteId;
|
|
171
|
-
|
|
172
|
-
this.consoleLogger(`ADT Pulse: Site ID -> ${siteId}`, 'log');
|
|
173
|
-
this.consoleLogger('ADT Pulse: Login success.', 'log');
|
|
174
|
-
|
|
175
|
-
deferred.resolve({
|
|
176
|
-
action: 'LOGIN',
|
|
177
|
-
success: true,
|
|
178
|
-
info: {
|
|
179
|
-
version: lastKnownVersion,
|
|
180
|
-
siteId: lastKnownSiteId,
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
return deferred.promise;
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* ADT Pulse logout.
|
|
197
|
-
*
|
|
198
|
-
* @returns {Q.Promise<object>}
|
|
199
|
-
*
|
|
200
|
-
* @since 1.0.0
|
|
201
|
-
*/
|
|
202
|
-
Pulse.prototype.logout = function logout() {
|
|
203
|
-
const deferred = Q.defer();
|
|
204
|
-
|
|
205
|
-
this.hasInternetWrapper(deferred, () => {
|
|
206
|
-
if (!authenticated) {
|
|
207
|
-
deferred.resolve({
|
|
208
|
-
action: 'LOGOUT',
|
|
209
|
-
success: true,
|
|
210
|
-
info: null,
|
|
211
|
-
});
|
|
212
|
-
} else {
|
|
213
|
-
this.consoleLogger('ADT Pulse: Logging out...', 'log');
|
|
214
|
-
|
|
215
|
-
request.get(
|
|
216
|
-
`https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/access/signout.jsp?networkid=${lastKnownSiteId}&partner=adt`,
|
|
217
|
-
this.generateRequestOptions({
|
|
218
|
-
headers: {
|
|
219
|
-
Referer: `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/summary/summary.jsp`,
|
|
220
|
-
},
|
|
221
|
-
}),
|
|
222
|
-
(error, response, body) => {
|
|
223
|
-
const regex = new RegExp(/(\/myhome\/)([0-9.-]+)(\/access\/signin\.jsp)(.*)/);
|
|
224
|
-
const responsePath = _.get(response, 'request.uri.path');
|
|
225
|
-
|
|
226
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${responsePath}`, 'log');
|
|
227
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${regex.test(responsePath)}`, 'log');
|
|
228
|
-
|
|
229
|
-
if (error || !regex.test(responsePath)) {
|
|
230
|
-
authenticated = true;
|
|
231
|
-
|
|
232
|
-
this.consoleLogger('ADT Pulse: Logout failed.', 'error');
|
|
233
|
-
|
|
234
|
-
deferred.reject({
|
|
235
|
-
action: 'LOGOUT',
|
|
236
|
-
success: false,
|
|
237
|
-
info: {
|
|
238
|
-
error,
|
|
239
|
-
message: this.getErrorMessage(body),
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
} else {
|
|
243
|
-
authenticated = false;
|
|
244
|
-
|
|
245
|
-
this.consoleLogger('ADT Pulse: Logout success.', 'log');
|
|
246
|
-
|
|
247
|
-
deferred.resolve({
|
|
248
|
-
action: 'LOGOUT',
|
|
249
|
-
success: true,
|
|
250
|
-
info: null,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
return deferred.promise;
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* ADT Pulse get device information.
|
|
263
|
-
*
|
|
264
|
-
* @returns {Q.Promise<object>}
|
|
265
|
-
*
|
|
266
|
-
* @since 1.0.0
|
|
267
|
-
*/
|
|
268
|
-
Pulse.prototype.getDeviceInformation = function getDeviceInformation() {
|
|
269
|
-
const deferred = Q.defer();
|
|
270
|
-
|
|
271
|
-
this.hasInternetWrapper(deferred, () => {
|
|
272
|
-
this.consoleLogger('ADT Pulse: Getting device information...', 'log');
|
|
273
|
-
|
|
274
|
-
request.get(
|
|
275
|
-
`https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/system/device.jsp?id=1`,
|
|
276
|
-
this.generateRequestOptions({
|
|
277
|
-
headers: {
|
|
278
|
-
Referer: `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/system/system.jsp`,
|
|
279
|
-
},
|
|
280
|
-
}),
|
|
281
|
-
(error, response, body) => {
|
|
282
|
-
const regex = new RegExp(/(\/myhome\/)([0-9.-]+)(\/system\/device\.jsp)(.*)/);
|
|
283
|
-
const responsePath = _.get(response, 'request.uri.path');
|
|
284
|
-
|
|
285
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${responsePath}`, 'log');
|
|
286
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${regex.test(responsePath)}`, 'log');
|
|
287
|
-
|
|
288
|
-
if (error || !regex.test(responsePath)) {
|
|
289
|
-
authenticated = false;
|
|
290
|
-
|
|
291
|
-
this.consoleLogger('ADT Pulse: Get device information failed.', 'error');
|
|
292
|
-
|
|
293
|
-
deferred.reject({
|
|
294
|
-
action: 'GET_DEVICE_INFO',
|
|
295
|
-
success: false,
|
|
296
|
-
info: {
|
|
297
|
-
error,
|
|
298
|
-
message: this.getErrorMessage(body),
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
} else {
|
|
302
|
-
const $ = cheerio.load(body);
|
|
303
|
-
const deviceName = $('td.InputFieldDescriptionL:contains("Name")').next().text().trim();
|
|
304
|
-
const deviceMake = $('td.InputFieldDescriptionL:contains("Manufacturer")').next().text().trim();
|
|
305
|
-
const deviceType = $('td.InputFieldDescriptionL:contains("Type")').next().text().trim();
|
|
306
|
-
|
|
307
|
-
this.consoleLogger('ADT Pulse: Get device information success.', 'log');
|
|
308
|
-
|
|
309
|
-
deferred.resolve({
|
|
310
|
-
action: 'GET_DEVICE_INFO',
|
|
311
|
-
success: true,
|
|
312
|
-
info: {
|
|
313
|
-
name: deviceName,
|
|
314
|
-
make: deviceMake,
|
|
315
|
-
type: deviceType,
|
|
316
|
-
},
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
return deferred.promise;
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* ADT Pulse get device status.
|
|
328
|
-
*
|
|
329
|
-
* @returns {Q.Promise<object>}
|
|
330
|
-
*
|
|
331
|
-
* @since 1.0.0
|
|
332
|
-
*/
|
|
333
|
-
Pulse.prototype.getDeviceStatus = function getDeviceStatus() {
|
|
334
|
-
const deferred = Q.defer();
|
|
335
|
-
|
|
336
|
-
this.hasInternetWrapper(deferred, () => {
|
|
337
|
-
this.consoleLogger('ADT Pulse: Getting device status...', 'log');
|
|
338
|
-
|
|
339
|
-
request.get(
|
|
340
|
-
`https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/ajax/orb.jsp`,
|
|
341
|
-
this.generateRequestOptions({
|
|
342
|
-
headers: {
|
|
343
|
-
Accept: '*/*',
|
|
344
|
-
Referer: `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/summary/summary.jsp`,
|
|
345
|
-
},
|
|
346
|
-
}),
|
|
347
|
-
(error, response, body) => {
|
|
348
|
-
const regex = new RegExp(/(\/myhome\/)([0-9.-]+)(\/ajax\/orb\.jsp)/);
|
|
349
|
-
const responsePath = _.get(response, 'request.uri.path');
|
|
350
|
-
|
|
351
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${responsePath}`, 'log');
|
|
352
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${regex.test(responsePath)}`, 'log');
|
|
353
|
-
|
|
354
|
-
if (error || !regex.test(responsePath) || body.indexOf('<html') > -1) {
|
|
355
|
-
authenticated = false;
|
|
356
|
-
|
|
357
|
-
this.consoleLogger('ADT Pulse: Get device status failed.', 'error');
|
|
358
|
-
|
|
359
|
-
deferred.reject({
|
|
360
|
-
action: 'GET_DEVICE_STATUS',
|
|
361
|
-
success: false,
|
|
362
|
-
info: {
|
|
363
|
-
error,
|
|
364
|
-
message: this.getErrorMessage(body),
|
|
365
|
-
},
|
|
366
|
-
});
|
|
367
|
-
} else {
|
|
368
|
-
const $ = cheerio.load(body);
|
|
369
|
-
const textSummary = $('#divOrbTextSummary span').text();
|
|
370
|
-
const theState = textSummary.replace(/([A-Z a-z ]+)(\.[ ]?)([A-Z a-z 0-9]*)(\.?)(.*)/g, '$1');
|
|
371
|
-
const theStatus = textSummary.replace(/([A-Z a-z ]+)(\.[ ]?)([A-Z a-z 0-9]*)(\.?)(.*)/g, '$3');
|
|
372
|
-
|
|
373
|
-
this.consoleLogger('ADT Pulse: Get device status success.', 'log');
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* These are the possible states and statuses.
|
|
377
|
-
*
|
|
378
|
-
* State:
|
|
379
|
-
* "Disarmed"
|
|
380
|
-
* "Armed Away"
|
|
381
|
-
* "Armed Stay"
|
|
382
|
-
* "Armed Night"
|
|
383
|
-
* "Status Unavailable"
|
|
384
|
-
* Status:
|
|
385
|
-
* "All Quiet"
|
|
386
|
-
* "1 Sensor Open" or "x Sensors Open"
|
|
387
|
-
* "Sensor Bypassed" or "Sensors Bypassed"
|
|
388
|
-
* "Sensor Tripped" or "Sensors Tripped"
|
|
389
|
-
* "Motion"
|
|
390
|
-
* "Uncleared Alarm"
|
|
391
|
-
* "Carbon Monoxide Alarm"
|
|
392
|
-
* "FIRE ALARM"
|
|
393
|
-
* "BURGLARY ALARM"
|
|
394
|
-
* "Sensor Problem"
|
|
395
|
-
* ""
|
|
396
|
-
*/
|
|
397
|
-
deferred.resolve({
|
|
398
|
-
action: 'GET_DEVICE_STATUS',
|
|
399
|
-
success: true,
|
|
400
|
-
info: {
|
|
401
|
-
summary: textSummary,
|
|
402
|
-
state: theState,
|
|
403
|
-
status: theStatus,
|
|
404
|
-
},
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
},
|
|
408
|
-
);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
return deferred.promise;
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* ADT Pulse set device status.
|
|
416
|
-
*
|
|
417
|
-
* Notes:
|
|
418
|
-
* - When Disarming, armState will be set to "disarmed". It will be set to "off" after re-login.
|
|
419
|
-
* - When Arming Night, armState will be set to "night+stay". It will be set to "night" after re-login.
|
|
420
|
-
* - If alarm occurred, you must Clear Alarm before setting to Armed Away/Stay/Night.
|
|
421
|
-
* - For ADT Pulse Canada, you must replace the "portal" sub-domain to "portal-ca" sub-domain.
|
|
422
|
-
* - "sat" code is now required for all set actions.
|
|
423
|
-
*
|
|
424
|
-
* Disarmed:
|
|
425
|
-
* - Arm Away (https://portal.adtpulse.com/myhome/quickcontrol/armDisarm.jsp?href=rest/adt/ui/client/security/setArmState&armstate=disarmed&arm=away&sat=).
|
|
426
|
-
* - Arm Stay (https://portal.adtpulse.com/myhome/quickcontrol/armDisarm.jsp?href=rest/adt/ui/client/security/setArmState&armstate=disarmed&arm=stay&sat=).
|
|
427
|
-
* - Arm Night (https://portal.adtpulse.com/myhome/quickcontrol/armDisarm.jsp?href=rest/adt/ui/client/security/setArmState&armstate=disarmed&arm=night&sat=).
|
|
428
|
-
* - Disarm (https://portal.adtpulse.com/myhome/quickcontrol/armDisarm.jsp?href=rest/adt/ui/client/security/setArmState&armstate=disarmed&arm=off&sat=).
|
|
429
|
-
* - Clear Alarm (https://portal.adtpulse.com/myhome/quickcontrol/armDisarm.jsp?href=rest/adt/ui/client/security/setArmState&armstate=disarmed+with+alarm&arm=off&sat=).
|
|
430
|
-
* Armed Away:
|
|
431
|
-
* - Disarm (https://portal.adtpulse.com/myhome/quickcontrol/armDisarm.jsp?href=rest/adt/ui/client/security/setArmState&armstate=away&arm=off&sat=).
|
|
432
|
-
* Armed Stay:
|
|
433
|
-
* - Disarm (https://portal.adtpulse.com/myhome/quickcontrol/armDisarm.jsp?href=rest/adt/ui/client/security/setArmState&armstate=stay&arm=off&sat=).
|
|
434
|
-
* Armed Night:
|
|
435
|
-
* - Disarm (https://portal.adtpulse.com/myhome/quickcontrol/armDisarm.jsp?href=rest/adt/ui/client/security/setArmState&armstate=night&arm=off&sat=).
|
|
436
|
-
*
|
|
437
|
-
* @param {string} armState - Can be "disarmed", "disarmed+with+alarm", "away", "stay", or "night".
|
|
438
|
-
* @param {string} arm - Can be "off", "away", "stay", or "night".
|
|
439
|
-
*
|
|
440
|
-
* @returns {Q.Promise<object>}
|
|
441
|
-
*
|
|
442
|
-
* @since 1.0.0
|
|
443
|
-
*/
|
|
444
|
-
Pulse.prototype.setDeviceStatus = function setDeviceStatus(armState, arm) {
|
|
445
|
-
const deferred = Q.defer();
|
|
446
|
-
|
|
447
|
-
this.hasInternetWrapper(deferred, () => {
|
|
448
|
-
const url1 = `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/summary/summary.jsp`;
|
|
449
|
-
|
|
450
|
-
this.consoleLogger('ADT Pulse: Setting device status...', 'log');
|
|
451
|
-
|
|
452
|
-
request.get(
|
|
453
|
-
url1,
|
|
454
|
-
this.generateRequestOptions(),
|
|
455
|
-
(error1, response1, body1) => {
|
|
456
|
-
const regex1 = new RegExp(/(\/myhome\/)([0-9.-]+)(\/summary\/summary\.jsp)(.*)/);
|
|
457
|
-
const responsePath1 = _.get(response1, 'request.uri.path');
|
|
458
|
-
|
|
459
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${responsePath1}`, 'log');
|
|
460
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${regex1.test(responsePath1)}`, 'log');
|
|
461
|
-
|
|
462
|
-
if (error1 || !regex1.test(responsePath1)) {
|
|
463
|
-
authenticated = false;
|
|
464
|
-
|
|
465
|
-
this.consoleLogger(`ADT Pulse: Set device status to ${arm} failed.`, 'error');
|
|
466
|
-
|
|
467
|
-
deferred.reject({
|
|
468
|
-
action: 'SET_DEVICE_STATUS',
|
|
469
|
-
success: false,
|
|
470
|
-
info: {
|
|
471
|
-
error: error1,
|
|
472
|
-
message: this.getErrorMessage(body1),
|
|
473
|
-
},
|
|
474
|
-
});
|
|
475
|
-
} else {
|
|
476
|
-
const $2 = cheerio.load(body1);
|
|
477
|
-
const onClick2 = $2('input[id^="security_button_"]').attr('onclick');
|
|
478
|
-
const satCode2 = (onClick2 !== undefined) ? onClick2.replace(/(.*)(&sat=)([0-9a-z-]*)('\))/g, '$3') : undefined;
|
|
479
|
-
const url2 = `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/quickcontrol/armDisarm.jsp`;
|
|
480
|
-
const arg2 = `?href=rest/adt/ui/client/security/setArmState&armstate=${armState}&arm=${arm}&sat=${satCode2}`;
|
|
481
|
-
|
|
482
|
-
request.get(
|
|
483
|
-
url2 + arg2,
|
|
484
|
-
this.generateRequestOptions({
|
|
485
|
-
headers: {
|
|
486
|
-
Referer: `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/summary/summary.jsp`,
|
|
487
|
-
},
|
|
488
|
-
}),
|
|
489
|
-
(error2, response2, body2) => {
|
|
490
|
-
const regex2 = new RegExp(/(\/myhome\/)([0-9.-]+)(\/quickcontrol\/armDisarm\.jsp)(.*)/);
|
|
491
|
-
const responsePath2 = _.get(response2, 'request.uri.path');
|
|
492
|
-
|
|
493
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${responsePath2}`, 'log');
|
|
494
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${regex2.test(responsePath2)}`, 'log');
|
|
495
|
-
|
|
496
|
-
if (error2 || !regex2.test(responsePath2)) {
|
|
497
|
-
authenticated = false;
|
|
498
|
-
|
|
499
|
-
this.consoleLogger(`ADT Pulse: Set device status to ${arm} failed.`, 'error');
|
|
500
|
-
|
|
501
|
-
deferred.reject({
|
|
502
|
-
action: 'SET_DEVICE_STATUS',
|
|
503
|
-
success: false,
|
|
504
|
-
info: {
|
|
505
|
-
error: error2,
|
|
506
|
-
message: this.getErrorMessage(body2),
|
|
507
|
-
},
|
|
508
|
-
});
|
|
509
|
-
} else {
|
|
510
|
-
const $3 = cheerio.load(body2);
|
|
511
|
-
const onClick3 = $3('input[id^="arm_button_"][value="Arm Anyway"]').attr('onclick');
|
|
512
|
-
const satCode3 = (onClick3 !== undefined) ? onClick3.replace(/(.*)(\?sat=)([0-9a-z-]*)(&href=)(.*)/g, '$3') : undefined;
|
|
513
|
-
|
|
514
|
-
const url3 = `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/quickcontrol/serv/RunRRACommand`;
|
|
515
|
-
const arg3 = `?sat=${satCode3}&href=rest/adt/ui/client/security/setForceArm&armstate=forcearm&arm=${arm}`;
|
|
516
|
-
|
|
517
|
-
// Check if system requires force arming.
|
|
518
|
-
if (['away', 'stay', 'night'].includes(arm) && onClick3 !== undefined && satCode3 !== undefined) {
|
|
519
|
-
this.consoleLogger('ADT Pulse: Some sensors are open or reporting motion. Arming Anyway...', 'warn');
|
|
520
|
-
|
|
521
|
-
request.get(
|
|
522
|
-
url3 + arg3,
|
|
523
|
-
this.generateRequestOptions({
|
|
524
|
-
headers: {
|
|
525
|
-
Accept: '*/*',
|
|
526
|
-
Referer: `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/quickcontrol/armDisarm.jsp`,
|
|
527
|
-
},
|
|
528
|
-
}),
|
|
529
|
-
(forceError, forceResponse, forceBody) => {
|
|
530
|
-
const forceRegex = new RegExp(/(\/myhome\/)([0-9.-]+)(\/quickcontrol\/serv\/RunRRACommand)(.*)/);
|
|
531
|
-
const forceResponsePath = _.get(forceResponse, 'request.uri.path');
|
|
532
|
-
|
|
533
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${forceResponsePath}`, 'log');
|
|
534
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${forceRegex.test(forceResponsePath)}`, 'log');
|
|
535
|
-
|
|
536
|
-
if (forceError || !forceRegex.test(forceResponsePath)) {
|
|
537
|
-
authenticated = false;
|
|
538
|
-
|
|
539
|
-
this.consoleLogger(`ADT Pulse: Set device status to ${arm} failed.`, 'error');
|
|
540
|
-
|
|
541
|
-
deferred.reject({
|
|
542
|
-
action: 'SET_DEVICE_STATUS',
|
|
543
|
-
success: false,
|
|
544
|
-
info: {
|
|
545
|
-
error: forceError,
|
|
546
|
-
message: this.getErrorMessage(forceBody),
|
|
547
|
-
},
|
|
548
|
-
});
|
|
549
|
-
} else {
|
|
550
|
-
this.consoleLogger(`ADT Pulse: Set device status to ${arm} success.`, 'log');
|
|
551
|
-
|
|
552
|
-
deferred.resolve({
|
|
553
|
-
action: 'SET_DEVICE_STATUS',
|
|
554
|
-
success: true,
|
|
555
|
-
info: {
|
|
556
|
-
forceArm: true,
|
|
557
|
-
previousArm: armState,
|
|
558
|
-
afterArm: arm,
|
|
559
|
-
},
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
},
|
|
563
|
-
);
|
|
564
|
-
} else {
|
|
565
|
-
this.consoleLogger(`ADT Pulse: Set device status to ${arm} success.`, 'log');
|
|
566
|
-
|
|
567
|
-
deferred.resolve({
|
|
568
|
-
action: 'SET_DEVICE_STATUS',
|
|
569
|
-
success: true,
|
|
570
|
-
info: {
|
|
571
|
-
forceArm: false,
|
|
572
|
-
previousArm: armState,
|
|
573
|
-
afterArm: arm,
|
|
574
|
-
},
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
},
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
},
|
|
582
|
-
);
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
return deferred.promise;
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* ADT Pulse get zone status.
|
|
590
|
-
*
|
|
591
|
-
* @returns {Q.Promise<object>}
|
|
592
|
-
*
|
|
593
|
-
* @since 1.0.0
|
|
594
|
-
*/
|
|
595
|
-
Pulse.prototype.getZoneStatus = function getZoneStatus() {
|
|
596
|
-
const deferred = Q.defer();
|
|
597
|
-
|
|
598
|
-
this.hasInternetWrapper(deferred, () => {
|
|
599
|
-
this.consoleLogger('ADT Pulse: Getting zone status...', 'log');
|
|
600
|
-
|
|
601
|
-
request.get(
|
|
602
|
-
`https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/ajax/orb.jsp`,
|
|
603
|
-
this.generateRequestOptions({
|
|
604
|
-
headers: {
|
|
605
|
-
Accept: '*/*',
|
|
606
|
-
Referer: `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/summary/summary.jsp`,
|
|
607
|
-
},
|
|
608
|
-
}),
|
|
609
|
-
(error, response, body) => {
|
|
610
|
-
const regex = new RegExp(/(\/myhome\/)([0-9.-]+)(\/ajax\/orb\.jsp)/);
|
|
611
|
-
const responsePath = _.get(response, 'request.uri.path');
|
|
612
|
-
|
|
613
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${responsePath}`, 'log');
|
|
614
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${regex.test(responsePath)}`, 'log');
|
|
615
|
-
|
|
616
|
-
if (error || !regex.test(responsePath) || body.indexOf('<html') > -1) {
|
|
617
|
-
authenticated = false;
|
|
618
|
-
|
|
619
|
-
this.consoleLogger('ADT Pulse: Get zone status failed.', 'error');
|
|
620
|
-
|
|
621
|
-
deferred.reject({
|
|
622
|
-
action: 'GET_ZONE_STATUS',
|
|
623
|
-
success: false,
|
|
624
|
-
info: {
|
|
625
|
-
error,
|
|
626
|
-
message: this.getErrorMessage(body),
|
|
627
|
-
},
|
|
628
|
-
});
|
|
629
|
-
} else {
|
|
630
|
-
const $ = cheerio.load(body);
|
|
631
|
-
const sensors = $('#orbSensorsList table tr.p_listRow').toArray();
|
|
632
|
-
|
|
633
|
-
const output = _.map(sensors, (sensor) => {
|
|
634
|
-
const theSensor = cheerio.load([].concat(sensor));
|
|
635
|
-
const theName = theSensor('a.p_deviceNameText').html();
|
|
636
|
-
const theZone = theSensor('span.p_grayNormalText').html();
|
|
637
|
-
const theState = theSensor('span.devStatIcon canvas').attr('icon');
|
|
638
|
-
|
|
639
|
-
const theZoneNumber = (theZone) ? theZone.replace(/(Zone)( | )([0-9]{1,2})/, '$3') : 0;
|
|
640
|
-
|
|
641
|
-
let theTag;
|
|
642
|
-
|
|
643
|
-
if (typeof theName === 'string' && theState !== 'devStatUnknown') {
|
|
644
|
-
const theNameLowercase = theName.toLowerCase();
|
|
645
|
-
const theOverrideSensor = _.find(this.overrideSensors, (overrideSensor) => overrideSensor.name.toLowerCase() === theNameLowercase);
|
|
646
|
-
|
|
647
|
-
if (theOverrideSensor !== undefined) {
|
|
648
|
-
const theOverrideSensorType = _.get(theOverrideSensor, 'type');
|
|
649
|
-
const allowedSensorTypes = ['sensor,glass', 'sensor,motion', 'sensor,co', 'sensor,fire', 'sensor,doorWindow'];
|
|
650
|
-
|
|
651
|
-
this.consoleLogger(`ADT Pulse: ${theName} sensor type is manually overridden to "${theOverrideSensorType}".`, 'warn');
|
|
652
|
-
|
|
653
|
-
theTag = (allowedSensorTypes.includes(theOverrideSensorType)) ? theOverrideSensorType : undefined;
|
|
654
|
-
} else if (theNameLowercase.match(/^(.*)(glass)(.*)$/g) !== null) {
|
|
655
|
-
theTag = 'sensor,glass';
|
|
656
|
-
} else if (theNameLowercase.match(/^(.*)(motion)(.*)$/g) !== null) {
|
|
657
|
-
theTag = 'sensor,motion';
|
|
658
|
-
} else if (theNameLowercase.match(/^(.*)(gas)(.*)$/g) !== null) {
|
|
659
|
-
theTag = 'sensor,co';
|
|
660
|
-
} else if (theNameLowercase.match(/^(.*)(smoke|heat)(.*)$/g) !== null) {
|
|
661
|
-
theTag = 'sensor,fire';
|
|
662
|
-
} else if (theNameLowercase.match(/^(.*)(door|window|dr|win|slider)(.*)$/g) !== null) {
|
|
663
|
-
theTag = 'sensor,doorWindow';
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Expected output.
|
|
669
|
-
*
|
|
670
|
-
* id: sensor-[integer]
|
|
671
|
-
* name: device name
|
|
672
|
-
* tags: sensor,[doorWindow,motion,glass,co,fire]
|
|
673
|
-
* state: devStatOK (device okay)
|
|
674
|
-
* devStatLowBatt (device low battery)
|
|
675
|
-
* devStatOpen (door/window opened)
|
|
676
|
-
* devStatMotion (detected motion)
|
|
677
|
-
* devStatTamper (glass broken or device tamper)
|
|
678
|
-
* devStatAlarm (detected CO/Smoke)
|
|
679
|
-
* devStatUnknown (device offline)
|
|
680
|
-
*/
|
|
681
|
-
return {
|
|
682
|
-
id: `sensor-${theZoneNumber}`,
|
|
683
|
-
name: theName || '',
|
|
684
|
-
tags: theTag || 'sensor',
|
|
685
|
-
state: theState || 'devStatUnknown',
|
|
686
|
-
};
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
this.consoleLogger('ADT Pulse: Get zone status success.', 'log');
|
|
690
|
-
|
|
691
|
-
deferred.resolve({
|
|
692
|
-
action: 'GET_ZONE_STATUS',
|
|
693
|
-
success: true,
|
|
694
|
-
info: output,
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
},
|
|
698
|
-
);
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
return deferred.promise;
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* ADT Pulse sync protocol.
|
|
706
|
-
*
|
|
707
|
-
* @returns {Q.Promise<object>}
|
|
708
|
-
*
|
|
709
|
-
* @since 1.0.0
|
|
710
|
-
*/
|
|
711
|
-
Pulse.prototype.performPortalSync = function performPortalSync() {
|
|
712
|
-
const deferred = Q.defer();
|
|
713
|
-
|
|
714
|
-
this.hasInternetWrapper(deferred, () => {
|
|
715
|
-
this.consoleLogger('ADT Pulse: Performing portal sync...', 'log');
|
|
716
|
-
|
|
717
|
-
request.get(
|
|
718
|
-
`https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/Ajax/SyncCheckServ?t=${Date.now()}`,
|
|
719
|
-
this.generateRequestOptions({
|
|
720
|
-
headers: {
|
|
721
|
-
Accept: '*/*',
|
|
722
|
-
Referer: `https://${countrySubDomain}.adtpulse.com/myhome/${lastKnownVersion}/summary/summary.jsp`,
|
|
723
|
-
},
|
|
724
|
-
}),
|
|
725
|
-
(error, response, body) => {
|
|
726
|
-
const regex = new RegExp(/(\/myhome\/)([0-9.-]+)(\/Ajax\/SyncCheckServ)(.*)/);
|
|
727
|
-
const responsePath = _.get(response, 'request.uri.path');
|
|
728
|
-
|
|
729
|
-
this.consoleLogger(`ADT Pulse: Response path -> ${responsePath}`, 'log');
|
|
730
|
-
this.consoleLogger(`ADT Pulse: Response path matches -> ${regex.test(responsePath)}`, 'log');
|
|
731
|
-
|
|
732
|
-
if (error || !regex.test(responsePath) || body.indexOf('<html') > -1) {
|
|
733
|
-
authenticated = false;
|
|
734
|
-
|
|
735
|
-
this.consoleLogger('ADT Pulse: Portal sync failed.', 'error');
|
|
736
|
-
|
|
737
|
-
deferred.reject({
|
|
738
|
-
action: 'SYNC',
|
|
739
|
-
success: false,
|
|
740
|
-
info: {
|
|
741
|
-
error,
|
|
742
|
-
message: this.getErrorMessage(body),
|
|
743
|
-
},
|
|
744
|
-
});
|
|
745
|
-
} else {
|
|
746
|
-
this.consoleLogger('ADT Pulse: Portal sync success.', 'log');
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* May return sync codes like this:
|
|
750
|
-
* 1-0-0
|
|
751
|
-
* 2-0-0
|
|
752
|
-
* [integer]-0-0
|
|
753
|
-
* [integer]-[integer]-0
|
|
754
|
-
*/
|
|
755
|
-
deferred.resolve({
|
|
756
|
-
action: 'SYNC',
|
|
757
|
-
success: true,
|
|
758
|
-
info: {
|
|
759
|
-
syncCode: body,
|
|
760
|
-
},
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
},
|
|
764
|
-
);
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
return deferred.promise;
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Internet available wrapper.
|
|
772
|
-
*
|
|
773
|
-
* @param {Q.Deferred} deferred - Used for rejecting promises.
|
|
774
|
-
* @param {Function} runFunction - Run function if internet is available.
|
|
775
|
-
*
|
|
776
|
-
* @since 1.0.0
|
|
777
|
-
*/
|
|
778
|
-
Pulse.prototype.hasInternetWrapper = function hasInternetWrapper(deferred, runFunction) {
|
|
779
|
-
const settings = {
|
|
780
|
-
timeout: 5000,
|
|
781
|
-
retries: 3,
|
|
782
|
-
domainName: `${countrySubDomain}.adtpulse.com`,
|
|
783
|
-
port: 53,
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
hasInternet(settings).then(runFunction).catch(() => {
|
|
787
|
-
this.consoleLogger(`ADT Pulse: Internet connection is offline or "https://${countrySubDomain}.adtpulse.com" is unavailable.`, 'error');
|
|
788
|
-
|
|
789
|
-
deferred.reject({
|
|
790
|
-
action: 'CONNECT',
|
|
791
|
-
success: false,
|
|
792
|
-
info: null,
|
|
793
|
-
});
|
|
794
|
-
});
|
|
795
|
-
};
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* Request options generator.
|
|
799
|
-
*
|
|
800
|
-
* @param {object} additionalOptions - Additional options.
|
|
801
|
-
*
|
|
802
|
-
* @returns {object}
|
|
803
|
-
*
|
|
804
|
-
* @since 1.0.0
|
|
805
|
-
*/
|
|
806
|
-
Pulse.prototype.generateRequestOptions = function generateRequestOptions(additionalOptions = {}) {
|
|
807
|
-
const options = {
|
|
808
|
-
jar,
|
|
809
|
-
headers: {
|
|
810
|
-
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
811
|
-
Host: `${countrySubDomain}.adtpulse.com`,
|
|
812
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 Edg/100.0.1185.44',
|
|
813
|
-
},
|
|
814
|
-
ciphers: [
|
|
815
|
-
'ECDHE-RSA-AES256-GCM-SHA384',
|
|
816
|
-
'ECDHE-RSA-AES128-GCM-SHA256',
|
|
817
|
-
].join(':'),
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
// Merge additional options.
|
|
821
|
-
return _.merge(options, additionalOptions);
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
/**
|
|
825
|
-
* ADT Pulse get error message.
|
|
826
|
-
*
|
|
827
|
-
* @param {string} responseBody - The response body.
|
|
828
|
-
*
|
|
829
|
-
* @returns {(null|string)}
|
|
830
|
-
*
|
|
831
|
-
* @since 1.0.0
|
|
832
|
-
*/
|
|
833
|
-
Pulse.prototype.getErrorMessage = function getErrorMessage(responseBody) {
|
|
834
|
-
// Returns array or null.
|
|
835
|
-
let errorMessage = (responseBody) ? responseBody.match(/<div id="warnMsgContents" class="p_signinWarning">(.*?)<\/div>/g) : null;
|
|
836
|
-
|
|
837
|
-
// Returns string.
|
|
838
|
-
errorMessage = (errorMessage) ? errorMessage[0] : '';
|
|
839
|
-
|
|
840
|
-
// Replace single line break with space.
|
|
841
|
-
errorMessage = errorMessage.replace(/<br ?\/?>/ig, ' ');
|
|
842
|
-
|
|
843
|
-
// Remove all HTML code.
|
|
844
|
-
errorMessage = errorMessage.replace(/(<([^>]+)>)/ig, '');
|
|
845
|
-
|
|
846
|
-
// If empty message, return null.
|
|
847
|
-
return errorMessage || null;
|
|
848
|
-
};
|
|
849
|
-
|
|
850
|
-
/**
|
|
851
|
-
* ADT Pulse console logger.
|
|
852
|
-
*
|
|
853
|
-
* @param {string} content - The message or content being recorded into the logs.
|
|
854
|
-
* @param {string} type - Can be "error", "warn", or "log".
|
|
855
|
-
*
|
|
856
|
-
* @since 1.0.0
|
|
857
|
-
*/
|
|
858
|
-
Pulse.prototype.consoleLogger = function consoleLogger(content, type) {
|
|
859
|
-
const that = this;
|
|
860
|
-
|
|
861
|
-
if (that.debug) {
|
|
862
|
-
switch (type) {
|
|
863
|
-
case 'error':
|
|
864
|
-
console.error(content);
|
|
865
|
-
break;
|
|
866
|
-
case 'warn':
|
|
867
|
-
console.warn(content);
|
|
868
|
-
break;
|
|
869
|
-
case 'log':
|
|
870
|
-
console.log(content);
|
|
871
|
-
break;
|
|
872
|
-
default:
|
|
873
|
-
break;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
};
|
|
877
|
-
|
|
878
|
-
module.exports = Pulse;
|