homey-api 1.5.30 → 1.7.0

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.
@@ -579,7 +579,7 @@
579
579
 
580
580
  }
581
581
 
582
- export class StorageAdapterNodeJS extends StorageAdapter {
582
+ export class StorageAdapterMemory extends StorageAdapter {
583
583
 
584
584
 
585
585
 
@@ -5637,6 +5637,15 @@
5637
5637
 
5638
5638
 
5639
5639
 
5640
+ ):
5641
+ boolean;
5642
+
5643
+ isReactNative(
5644
+
5645
+
5646
+
5647
+
5648
+
5640
5649
  ):
5641
5650
  boolean;
5642
5651
 
@@ -5844,7 +5853,7 @@
5844
5853
 
5845
5854
  }
5846
5855
 
5847
- export class StorageAdapterNodeJS extends StorageAdapter {
5856
+ export class StorageAdapterMemory extends StorageAdapter {
5848
5857
 
5849
5858
 
5850
5859
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Abstract storage adapter. To be extended by your own, or {@link AthomCloudAPI.StorageAdapterBrowser} or {@link AthomCloudAPI.StorageAdapterNodeJS}.
4
+ * Abstract storage adapter. To be extended by your own, or {@link AthomCloudAPI.StorageAdapterBrowser} or {@link AthomCloudAPI.StorageAdapterMemory}.
5
5
  * @class
6
6
  * @memberof AthomCloudAPI
7
7
  */
@@ -3,12 +3,12 @@
3
3
  const StorageAdapter = require('./StorageAdapter');
4
4
 
5
5
  /**
6
- * In-memory storage adapter for Node.js.
6
+ * In-memory storage adapter for Node.js or React Native.
7
7
  * @class
8
8
  * @extends StorageAdapter
9
9
  * @memberof AthomCloudAPI
10
10
  */
11
- class StorageAdapterNodeJS extends StorageAdapter {
11
+ class StorageAdapterMemory extends StorageAdapter {
12
12
 
13
13
  constructor() {
14
14
  super();
@@ -34,4 +34,4 @@ class StorageAdapterNodeJS extends StorageAdapter {
34
34
 
35
35
  }
36
36
 
37
- module.exports = StorageAdapterNodeJS;
37
+ module.exports = StorageAdapterMemory;
@@ -9,7 +9,7 @@ const Homey = require('./AthomCloudAPI/Homey');
9
9
  const Token = require('./AthomCloudAPI/Token');
10
10
  const StorageAdapter = require('./AthomCloudAPI/StorageAdapter');
11
11
  const StorageAdapterBrowser = require('./AthomCloudAPI/StorageAdapterBrowser');
12
- const StorageAdapterNodeJS = require('./AthomCloudAPI/StorageAdapterNodeJS');
12
+ const StorageAdapterMemory = require('./AthomCloudAPI/StorageAdapterMemory');
13
13
 
14
14
  class AthomCloudAPI extends API {
15
15
 
@@ -18,7 +18,7 @@ class AthomCloudAPI extends API {
18
18
  static Token = Token;
19
19
  static StorageAdapter = StorageAdapter;
20
20
  static StorageAdapterBrowser = StorageAdapterBrowser;
21
- static StorageAdapterNodeJS = StorageAdapterNodeJS;
21
+ static StorageAdapterMemory = StorageAdapterMemory;
22
22
 
23
23
  static SPECIFICATION = require('../assets/specifications/AthomCloudAPI.json');
24
24
  static SPECIFICATION_URL = 'https://api.athom.com/specification.json';
@@ -35,7 +35,7 @@ and login on that user's Homey.`;
35
35
  const AthomCloudAPI = require('homey-api/lib/AthomCloudAPI');
36
36
 
37
37
  // Create an AthomCloudAPI instance
38
- const api = new AthomCloudAPI({
38
+ const cloudApi = new {@link AthomCloudAPI AthomCloudAPI}({
39
39
  clientId: '5a8d4ca6eb9f7a2c9d6ccf6d',
40
40
  clientSecret: 'e3ace394af9f615857ceaa61b053f966ddcfb12a',
41
41
  redirectUrl: 'http://localhost',
@@ -43,30 +43,38 @@ const api = new AthomCloudAPI({
43
43
 
44
44
  // Check if we're logged in
45
45
  // If not, redirect the user to the OAuth2 dialog
46
- const loggedIn = await api.isLoggedIn();
46
+ const loggedIn = await {@link AthomCloudAPI cloudApi}.{@link AthomCloudAPI#isLoggedIn isLoggedIn}();
47
47
  if (!loggedIn) {
48
- if (api.hasAuthorizationCode()) {
49
- const token = await api.authenticateWithAuthorizationCode();
48
+ if ({@link AthomCloudAPI cloudApi}.{@link AthomCloudAPI#hasAuthorizationCode hasAuthorizationCode}()) {
49
+ const token = await {@link AthomCloudAPI cloudApi}.{@link AthomCloudAPI#authenticateWithAuthorizationCode authenticateWithAuthorizationCode}();
50
50
  } else {
51
- window.location.href = api.getLoginUrl();
51
+ window.location.href = {@link AthomCloudAPI cloudApi}.{@link AthomCloudAPI#getLoginUrl getLoginUrl}();
52
52
  return;
53
53
  }
54
54
  }
55
55
 
56
56
  // Get the logged in user
57
- const user = await cloudApi.getAuthenticatedUser();
57
+ const user = await {@link AthomCloudAPI cloudApi}.{@link AthomCloudAPI#getAuthenticatedUser getAuthenticatedUser}();
58
58
 
59
59
  // Get the first Homey of the logged in user
60
- const homey = await user.getFirstHomey();
60
+ const homey = await {@link AthomCloudAPI.User user}.{@link AthomCloudAPI.User#getFirstHomey getFirstHomey}();
61
61
 
62
62
  // Create a session on this Homey
63
- const homeyApi = await homey.authenticate();
63
+ const homeyApi = await {@link AthomCloudAPI.Homey homey}.{@link AthomCloudAPI.Homey#authenticate authenticate}();
64
64
 
65
- // Loop all devices
66
- const devices = await homeyApi.devices.getDevices();
67
- for(const device of Object.values(devices)) {
65
+ // Get all Zones from ManagerZones
66
+ const zones = await {@link HomeyAPIV2 homeyApi}.{@link HomeyAPIV2.ManagerZones zones}.{@link HomeyAPIV2.ManagerZones#getZones getZones}();
67
+
68
+ // Get all Devices from ManagerDevices
69
+ const devices = await {@link HomeyAPIV2 homeyApi}.{@link HomeyAPIV2.ManagerDevices devices}.{@link HomeyAPIV2.ManagerDevices#getDevices getDevices}();
70
+
71
+ // Turn all devices on
72
+ for(const {@link HomeyAPIV2.ManagerDevices.Device device} of Object.values(devices)) {
68
73
  // Turn device on
69
- await device.setCapabilityValue({ capabilityId: 'onoff', value: true });
74
+ await {@link HomeyAPIV2.ManagerDevices.Device device}.{@link HomeyAPIV2.ManagerDevices.Device#setCapabilityValue setCapabilityValue}({
75
+ capabilityId: 'onoff',
76
+ value: true,
77
+ });
70
78
  }`;
71
79
 
72
80
  static JSDOC_PARAMS = `
@@ -75,7 +83,7 @@ for(const device of Object.values(devices)) {
75
83
  @param {string} opts.redirectUrl
76
84
  @param {boolean} [opts.autoRefreshTokens=true]
77
85
  @param {AthomCloudAPI.Token} [opts.token=null]
78
- @param {AthomCloudAPI.StorageAdapter} [opts.store={@link AthomCloudAPI.StorageAdapterBrowser} or {@link AthomCloudAPI.StorageAdapterNodeJS}]`;
86
+ @param {AthomCloudAPI.StorageAdapter} [opts.store={@link AthomCloudAPI.StorageAdapterBrowser} or {@link AthomCloudAPI.StorageAdapterMemory}]`;
79
87
 
80
88
  constructor({
81
89
  clientId,
@@ -85,7 +93,7 @@ for(const device of Object.values(devices)) {
85
93
  token = null,
86
94
  store = Util.isBrowser()
87
95
  ? new StorageAdapterBrowser()
88
- : new StorageAdapterNodeJS(),
96
+ : new StorageAdapterMemory(),
89
97
  ...args
90
98
  } = {}) {
91
99
  super({ ...args });
@@ -304,10 +312,11 @@ for(const device of Object.values(devices)) {
304
312
  body.append('grant_type', 'client_credentials');
305
313
 
306
314
  const response = await Util.fetch(`${this.baseUrl}/oauth2/token`, {
307
- body,
315
+ body: body.toString(),
308
316
  method: 'post',
309
317
  headers: {
310
318
  Authorization: `Basic ${Util.base64(`${this.__clientId}:${this.__clientSecret}`)}`,
319
+ 'Content-Type': 'application/x-www-form-urlencoded',
311
320
  },
312
321
  });
313
322
 
@@ -332,6 +341,13 @@ for(const device of Object.values(devices)) {
332
341
  return this.__token;
333
342
  }
334
343
 
344
+ /**
345
+ * Authenticate with an authorization code.
346
+ * @param {Object} [opts]
347
+ * @param {String} opts.code - Default to `?code=...` when in a browser.
348
+ * @param {Boolean} [opts.removeCodeFromHistory=true] - Remove `?code=...` from the URL in the address bar.
349
+ * @returns {Promise<AthomCloudAPI.Token>}
350
+ */
335
351
  async authenticateWithAuthorizationCode({
336
352
  code,
337
353
  removeCodeFromHistory = true,
@@ -358,10 +374,11 @@ for(const device of Object.values(devices)) {
358
374
  body.append('code', code);
359
375
 
360
376
  const response = await Util.fetch(`${this.baseUrl}/oauth2/token`, {
361
- body,
377
+ body: body.toString(),
362
378
  method: 'post',
363
379
  headers: {
364
380
  Authorization: `Basic ${Util.base64(`${this.__clientId}:${this.__clientSecret}`)}`,
381
+ 'Content-Type': 'application/x-www-form-urlencoded',
365
382
  },
366
383
  });
367
384
 
@@ -418,10 +435,11 @@ for(const device of Object.values(devices)) {
418
435
  body.append('password', password);
419
436
 
420
437
  const response = await Util.fetch(`${this.baseUrl}/oauth2/token`, {
421
- body,
438
+ body: body.toString(),
422
439
  method: 'post',
423
440
  headers: {
424
441
  Authorization: `Basic ${Util.base64(`${this.__clientId}:${this.__clientSecret}`)}`,
442
+ 'Content-Type': 'application/x-www-form-urlencoded',
425
443
  },
426
444
  });
427
445
 
@@ -462,10 +480,11 @@ for(const device of Object.values(devices)) {
462
480
  body.append('refresh_token', this.__token.refresh_token);
463
481
 
464
482
  const response = await Util.fetch(`${this.baseUrl}/oauth2/token`, {
465
- body,
483
+ body: body.toString(),
466
484
  method: 'post',
467
485
  headers: {
468
486
  Authorization: `Basic ${Util.base64(`${this.__clientId}:${this.__clientSecret}`)}`,
487
+ 'Content-Type': 'application/x-www-form-urlencoded',
469
488
  },
470
489
  });
471
490
 
@@ -500,6 +519,76 @@ for(const device of Object.values(devices)) {
500
519
  return this.__refreshTokenPromise;
501
520
  }
502
521
 
522
+ /**
523
+ * Update the currently authenticated user.
524
+ *
525
+ * @private
526
+ * @param {Object} [opts]
527
+ * @param {String} [opts.firstname]
528
+ * @param {String} [opts.lastname]
529
+ * @param {String} [opts.email]
530
+ * @returns {Promise<AthomCloudAPI.User>}
531
+ */
532
+ async updateUserMe({
533
+ firstname,
534
+ lastname,
535
+ email,
536
+ }) {
537
+ const me = await this.getAuthenticatedUser();
538
+ return this.updateUser({
539
+ id: me._id,
540
+ user: {
541
+ firstname,
542
+ lastname,
543
+ email,
544
+ },
545
+ });
546
+ }
547
+
548
+ /**
549
+ * Update the currently authenticated user's avatar.
550
+ *
551
+ * @private
552
+ * @param {Buffer} imageBuffer Buffer of the new avatat
553
+ * @param {"jpg"|"jpeg"|"png"|"gif"} imageType Type of the new avatar
554
+ * @returns {Promise<Object>}
555
+ */
556
+ async updateUserMeAvatar(imageBuffer, imageType) {
557
+ if (!Buffer.isBuffer(imageBuffer)) {
558
+ throw new Error('Invalid Image. Expected Buffer.');
559
+ }
560
+
561
+ if (!imageType) {
562
+ throw new Error('Missing Image Type');
563
+ }
564
+
565
+ if (!['jpg', 'png', 'gif'].includes(imageType)) {
566
+ throw new Error(`Invalid Image Type: ${imageType}`);
567
+ }
568
+
569
+ if (imageType === 'jpg') {
570
+ imageType = 'jpeg';
571
+ }
572
+
573
+ const me = await this.getAuthenticatedUser();
574
+ const body = Buffer.concat([
575
+ Buffer.from(`--__X_HOMEY_BOUNDARY__\r\nContent-Disposition: form-data; name="avatar"; filename="avatar"\r\nContent-Type: image/${imageType}\r\n\r\n`),
576
+ Buffer.from(imageBuffer),
577
+ Buffer.from('\r\n--__X_HOMEY_BOUNDARY__--\r\n'),
578
+ ]);
579
+
580
+ return this.call({
581
+ method: 'POST',
582
+ path: `/user/${me._id}/avatar`,
583
+ headers: {
584
+ 'Content-Type': 'multipart/form-data; boundary="__X_HOMEY_BOUNDARY__"',
585
+ 'Content-Length': body.length,
586
+ },
587
+ body,
588
+ bodyJSON: false,
589
+ });
590
+ }
591
+
503
592
  }
504
593
 
505
594
  module.exports = AthomCloudAPI;
@@ -24,6 +24,14 @@ class Device extends Item {
24
24
  * @param {number|boolean|string} listener.value
25
25
  * @returns {HomeyAPIV2.ManagerDevices.Device.DeviceCapability}
26
26
  * @function HomeyAPIV2.ManagerDevices.Device#makeCapabilityInstance
27
+ * @example
28
+ *
29
+ * const onOffInstance = device.makeCapabilityInstance('onoff', value => {
30
+ * console.log('Device onoff changed to:', value);
31
+ * });
32
+ *
33
+ * // Turn on
34
+ * onOffInstance.setValue(true).catch(console.error);
27
35
  */
28
36
  makeCapabilityInstance(capabilityId, listener) {
29
37
  this.connect().catch(err => {
@@ -1,9 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const fetch = require('node-fetch');
4
-
5
3
  const API = require('./API');
6
4
  const APIError = require('./APIError');
5
+ const Util = require('./Util');
7
6
 
8
7
  class HomeyCloudAPI extends API {
9
8
 
@@ -26,7 +25,7 @@ class HomeyCloudAPI extends API {
26
25
  * @returns {string} result.region - e.g. `eu-central-1`
27
26
  */
28
27
  static async getClosestRegion() {
29
- const res = await fetch(`https://${HomeyCloudAPI.SPECIFICATION.host}`);
28
+ const res = await Util.fetch(`https://${HomeyCloudAPI.SPECIFICATION.host}`);
30
29
  if (!res.ok) {
31
30
  throw new Error(res.statusText || 'Unknown Error');
32
31
  }
@@ -56,7 +55,7 @@ class HomeyCloudAPI extends API {
56
55
  * @returns {Promise<object>} result
57
56
  */
58
57
  async getSystemStatus({ secret }) {
59
- const res = await fetch(`${this.baseUrl}/api/system/status`, {
58
+ const res = await Util.fetch(`${this.baseUrl}/api/system/status`, {
60
59
  headers: {
61
60
  'X-Homey-Secret': secret,
62
61
  },
package/lib/Util.js CHANGED
@@ -18,13 +18,22 @@ class Util {
18
18
  * @returns {Promise}
19
19
  */
20
20
  static async fetch(...args) {
21
- // If in a browser
21
+ if (this.isReactNative()) {
22
+ return fetch(...args);
23
+ }
24
+
22
25
  if (this.isBrowser()) {
23
26
  return window.fetch(...args);
24
27
  }
25
28
 
26
- const fetch = require('node-fetch');
27
- return fetch(...args);
29
+ if (this.isNodeJS()) {
30
+ const fetch = require('node-fetch');
31
+ return fetch(...args);
32
+ }
33
+
34
+ if (typeof fetch !== 'undefined') {
35
+ return fetch(...args);
36
+ }
28
37
  }
29
38
 
30
39
  /**
@@ -81,17 +90,26 @@ class Util {
81
90
  return window.location.protocol === 'http:';
82
91
  }
83
92
 
93
+ /**
94
+ * @returns {boolean}
95
+ */
96
+ static isReactNative() {
97
+ return (typeof navigator !== 'undefined' && navigator.product === 'ReactNative');
98
+ }
99
+
84
100
  /**
85
101
  * @returns {boolean}
86
102
  */
87
103
  static isBrowser() {
88
- return (typeof window !== 'undefined');
104
+ if (this.isReactNative()) return false;
105
+ return (typeof document !== 'undefined' && typeof document.window !== 'undefined');
89
106
  }
90
107
 
91
108
  /**
92
109
  * @returns {boolean}
93
110
  */
94
111
  static isNodeJS() {
112
+ if (this.isReactNative()) return false;
95
113
  return (typeof process !== 'undefined');
96
114
  }
97
115
 
@@ -119,19 +137,58 @@ class Util {
119
137
  }
120
138
 
121
139
  /**
140
+ * This method encodes a string into a base64 string.
141
+ * It's provided as Util because Node.js uses `Buffer`,
142
+ * browsers use `btoa` and React Native doesn't provide anything.
122
143
  * @param {string} input - Input
123
144
  * @returns {string} - Base64 encoded output
124
145
  */
125
- static base64(str) {
126
- if (typeof btoa === 'function') {
127
- return btoa(str);
146
+ static base64(s) {
147
+ function btoaLookup(index) {
148
+ if (index >= 0 && index < 64) {
149
+ const keystr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
150
+ return keystr[index];
151
+ }
152
+
153
+ // Throw INVALID_CHARACTER_ERR exception here -- won't be hit in the tests.
154
+ return undefined;
128
155
  }
129
156
 
130
- if (typeof Buffer !== 'undefined') {
131
- return Buffer.from(str).toString('base64');
157
+ if (typeof s !== 'string') {
158
+ throw new Error('Invalid Input');
132
159
  }
133
160
 
134
- throw new Error('No Base64 Methods Available');
161
+ let i;
162
+
163
+ // "The btoa() method must throw an "InvalidCharacterError" DOMException if
164
+ // data contains any character whose code point is greater than U+00FF."
165
+ for (i = 0; i < s.length; i++) {
166
+ if (s.charCodeAt(i) > 255) {
167
+ return null;
168
+ }
169
+ }
170
+ let out = '';
171
+ for (i = 0; i < s.length; i += 3) {
172
+ const groupsOfSix = [undefined, undefined, undefined, undefined];
173
+ groupsOfSix[0] = s.charCodeAt(i) >> 2;
174
+ groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4;
175
+ if (s.length > i + 1) {
176
+ groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4;
177
+ groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2;
178
+ }
179
+ if (s.length > i + 2) {
180
+ groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6;
181
+ groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f;
182
+ }
183
+ for (let j = 0; j < groupsOfSix.length; j++) {
184
+ if (typeof groupsOfSix[j] === 'undefined') {
185
+ out += '=';
186
+ } else {
187
+ out += btoaLookup(groupsOfSix[j]);
188
+ }
189
+ }
190
+ }
191
+ return out;
135
192
  }
136
193
 
137
194
  /**
@@ -161,6 +218,10 @@ class Util {
161
218
  return process.env[key] || null;
162
219
  }
163
220
 
221
+ if (this.isReactNative()) {
222
+ return null;
223
+ }
224
+
164
225
  return null;
165
226
  }
166
227
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homey-api",
3
- "version": "1.5.30",
3
+ "version": "1.7.0",
4
4
  "description": "Homey API",
5
5
  "main": "index.js",
6
6
  "types": "assets/types/homey-api.d.ts",
@@ -53,7 +53,7 @@
53
53
  "eslint": "^7.32.0",
54
54
  "eslint-config-athom": "^2.1.1",
55
55
  "fs-extra": "^10.0.0",
56
- "homey-jsdoc-template": "github:athombv/homey-jsdoc-template#1.4",
56
+ "homey-jsdoc-template": "github:athombv/homey-jsdoc-template#1.5.1",
57
57
  "http-server": "^0.12.3",
58
58
  "jsdoc": "^3.6.7",
59
59
  "jsdoc-to-markdown": "^7.1.0",