homey-api 3.4.19 → 3.4.21

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.
@@ -3775,6 +3775,33 @@
3775
3775
  "idCamelCase": "zwave",
3776
3776
  "items": {},
3777
3777
  "operations": {
3778
+ "getOptionRegionOverride": {
3779
+ "method": "GET",
3780
+ "path": "/option/regionOverride",
3781
+ "scopes": [
3782
+ "homey.system.readonly"
3783
+ ]
3784
+ },
3785
+ "setOptionRegionOverride": {
3786
+ "method": "PUT",
3787
+ "path": "/option/regionOverride",
3788
+ "scopes": [
3789
+ "homey.system"
3790
+ ],
3791
+ "parameters": {
3792
+ "value": {
3793
+ "in": "body",
3794
+ "required": true
3795
+ }
3796
+ }
3797
+ },
3798
+ "unsetOptionRegionOverride": {
3799
+ "method": "DELETE",
3800
+ "path": "/option/regionOverride",
3801
+ "scopes": [
3802
+ "homey.system"
3803
+ ]
3804
+ },
3778
3805
  "getState": {
3779
3806
  "method": "get",
3780
3807
  "path": "/state",
@@ -2634,6 +2634,12 @@ export namespace HomeyAPIV3Cloud {
2634
2634
  }
2635
2635
 
2636
2636
  export class ManagerZwave extends HomeyAPIV3.ManagerZwave {
2637
+ getOptionRegionOverride(): Promise<any>;
2638
+
2639
+ setOptionRegionOverride(opts: { value: any }): Promise<any>;
2640
+
2641
+ unsetOptionRegionOverride(): Promise<any>;
2642
+
2637
2643
  getState(): Promise<any>;
2638
2644
 
2639
2645
  runCommand(opts: {
@@ -8640,6 +8640,12 @@ export namespace HomeyAPIV3Cloud {
8640
8640
  }
8641
8641
 
8642
8642
  export class ManagerZwave extends HomeyAPIV3.ManagerZwave {
8643
+ getOptionRegionOverride(): Promise<any>;
8644
+
8645
+ setOptionRegionOverride(opts: { value: any }): Promise<any>;
8646
+
8647
+ unsetOptionRegionOverride(): Promise<any>;
8648
+
8643
8649
  getState(): Promise<any>;
8644
8650
 
8645
8651
  runCommand(opts: {
@@ -324,7 +324,7 @@ for(const {@link HomeyAPIV2.ManagerDevices.Device device} of Object.values(devic
324
324
  ...props
325
325
  }) {
326
326
  if (statusCode === 401 && body.error === 'invalid_token') {
327
- if (this.__autoRefreshTokens && !request.context.retryAfterRefresh) {
327
+ if (this.__autoRefreshTokens && !request.context.isRetryAfterRefresh) {
328
328
  this.__debug('Refreshing token...');
329
329
  await this.authenticateWithRefreshToken();
330
330
 
@@ -333,7 +333,7 @@ for(const {@link HomeyAPIV2.ManagerDevices.Device device} of Object.values(devic
333
333
  ...request,
334
334
  context: {
335
335
  ...request.context,
336
- retryAfterRefresh: true,
336
+ isRetryAfterRefresh: true,
337
337
  },
338
338
  });
339
339
  }
@@ -107,6 +107,7 @@ class Manager extends EventEmitter {
107
107
  $body = {},
108
108
  $query = {},
109
109
  $headers = {},
110
+ shouldRetry,
110
111
  ...args
111
112
  } = {}) => {
112
113
  let { path } = operation;
@@ -215,6 +216,7 @@ class Manager extends EventEmitter {
215
216
  body,
216
217
  query,
217
218
  headers,
219
+ shouldRetry,
218
220
  ...args,
219
221
  });
220
222
 
@@ -244,7 +246,7 @@ class Manager extends EventEmitter {
244
246
  }
245
247
  }
246
248
 
247
- async __request({ $cache, $updateCache, $timeout, $socket, operationId, operation, path, body, headers, ...args }) {
249
+ async __request({ $cache, $updateCache, $timeout, $socket, operationId, operation, path, body, headers, shouldRetry, ...args }) {
248
250
  let result;
249
251
  const benchmark = Util.benchmark();
250
252
 
@@ -323,6 +325,7 @@ class Manager extends EventEmitter {
323
325
  body,
324
326
  path: `/api/manager/${this.constructor.ID}${path}`,
325
327
  method: operation.method,
328
+ shouldRetry,
326
329
  });
327
330
  }
328
331
 
@@ -53,6 +53,8 @@ class HomeyAPIV3 extends HomeyAPI {
53
53
  }) {
54
54
  super({ properties, ...props });
55
55
 
56
+ this.__refreshMap = {};
57
+
56
58
  Object.defineProperty(this, '__baseUrl', {
57
59
  value: null,
58
60
  enumerable: false,
@@ -412,7 +414,17 @@ class HomeyAPIV3 extends HomeyAPI {
412
414
  return promise;
413
415
  }
414
416
 
415
- async call({ $timeout = 10000, method, headers, path, body, json = true, retryAfterRefresh = false }) {
417
+ async call({
418
+ $timeout = 10000,
419
+ method,
420
+ headers,
421
+ path,
422
+ body,
423
+ json = true,
424
+ isRetryAfterRefresh = false,
425
+ shouldRetry = true,
426
+ }) {
427
+ const token = this.__token;
416
428
  const baseUrl = await this.baseUrl;
417
429
 
418
430
  method = String(method).toUpperCase();
@@ -422,11 +434,12 @@ class HomeyAPIV3 extends HomeyAPI {
422
434
  'X-Homey-ID': this.id,
423
435
  };
424
436
 
425
- if (this.__token) {
426
- headers['Authorization'] = `Bearer ${this.__token}`;
437
+ if (token) {
438
+ headers['Authorization'] = `Bearer ${token}`;
427
439
  }
428
440
 
429
441
  const originalBody = body;
442
+
430
443
  if (['PUT', 'POST'].includes(method)) {
431
444
  if (body && json === true) {
432
445
  headers['Content-Type'] = 'application/json';
@@ -470,26 +483,23 @@ class HomeyAPIV3 extends HomeyAPI {
470
483
  }
471
484
 
472
485
  if (!res.ok) {
473
- if (resBodyJson) {
474
- // If Session Expired, clear the stored token
475
- if (resStatusCode === 401) {
476
- this.__debug('Session expired, invalidating token...');
477
- await this.logout();
478
- }
486
+ // If Session Expired, clear the stored token
487
+ if (resStatusCode === 401 && shouldRetry === true && token != null) {
488
+ this.__debug('Session expired');
489
+ await this.refreshForToken(token, isRetryAfterRefresh);
479
490
 
480
- // If Session Expired, try to refresh the Token
481
- if (resStatusCode === 401 && !retryAfterRefresh) {
482
- this.__debug('Session expired, refreshing...');
483
- await this.login();
491
+ if (!isRetryAfterRefresh) {
484
492
  return this.call({
485
493
  method,
486
494
  headers,
487
495
  path,
488
496
  body: originalBody,
489
- retryAfterRefresh: true,
497
+ isRetryAfterRefresh: true,
490
498
  });
491
499
  }
500
+ }
492
501
 
502
+ if (resBodyJson) {
493
503
  throw new HomeyAPIError(
494
504
  {
495
505
  error: resBodyJson.error,
@@ -546,11 +556,13 @@ class HomeyAPIV3 extends HomeyAPI {
546
556
  const token = await this.users.login({
547
557
  $socket: false,
548
558
  token: jwtToken,
559
+ shouldRetry: false,
549
560
  });
550
561
  this.__token = token;
551
562
 
552
563
  const session = await this.sessions.getSessionMe({
553
564
  $socket: false,
565
+ shouldRetry: false,
554
566
  });
555
567
  this.__session = session;
556
568
 
@@ -564,14 +576,14 @@ class HomeyAPIV3 extends HomeyAPI {
564
576
  });
565
577
 
566
578
  this.__loginPromise
567
- .then(() => {})
579
+ .then(() => {
580
+ this.__loginPromise = null;
581
+ })
568
582
  .catch(err => {
569
583
  this.__debug('Error Logging In:', err);
584
+ this.__loginPromise = null;
570
585
  this.__token = null;
571
586
  this.__session = null;
572
- })
573
- .finally(() => {
574
- this.__loginPromise = null;
575
587
  });
576
588
  }
577
589
 
@@ -579,11 +591,49 @@ class HomeyAPIV3 extends HomeyAPI {
579
591
  }
580
592
 
581
593
  async logout() {
594
+ this.__token = null;
595
+ this.__session = null;
596
+
582
597
  await this.__setStore({
583
598
  token: null,
599
+ session: null,
584
600
  });
601
+ }
585
602
 
586
- this.__token = null;
603
+ async refreshForToken(token, isRetryAfterRefresh = false) {
604
+ if (this.__token === token && this.__token !== null && this.__refreshMap[token] == null) {
605
+ this.__refreshMap[token] = Promise.resolve().then(async () => {
606
+ await this.__setStore({
607
+ token: null,
608
+ session: null,
609
+ });
610
+
611
+ if (!isRetryAfterRefresh) {
612
+ this.__debug('Refreshing token...');
613
+ // If the login fails, the tokens are cleared on the instance. We don't call logout and clear
614
+ // the token on the instance because that could cause a handshake client to fire with a null
615
+ // token. Handshake client also needs to attempt to refresh the token, and if the refresh was
616
+ // already started, it should reuse this promise. That also goes the other way around.
617
+ await this.login();
618
+ }
619
+ });
620
+
621
+ this.__refreshMap[token]
622
+ .then(() => {})
623
+ .catch(err => {
624
+ this.__debug('Error Refreshing Token:', err);
625
+ })
626
+ .finally(() => {
627
+ // Delete after 30 seconds some requests might still be pending an they should be able
628
+ // to receive a rejected promise for this token.
629
+ this.__refreshMap[token+'timeout'] = setTimeout(() => {
630
+ delete this.__refreshMap[token];
631
+ delete this.__refreshMap[token+'timeout'];
632
+ }, 30 * 1000);
633
+ });
634
+ }
635
+
636
+ await this.__refreshMap[token];
587
637
  }
588
638
 
589
639
  /**
@@ -879,16 +929,16 @@ class HomeyAPIV3 extends HomeyAPI {
879
929
  });
880
930
  };
881
931
 
882
- const handshakeClient = async () => {
932
+ const handshakeClient = async token => {
883
933
  return new Promise((resolve, reject) => {
884
934
  this.__socket.emit(
885
935
  'handshakeClient',
886
936
  {
887
- token: this.__token,
937
+ token,
888
938
  homeyId: this.id,
889
939
  },
890
940
  (err, result) => {
891
- if (err != null) {
941
+ if (err != null) {
892
942
  if (typeof err === 'object') {
893
943
  err = new HomeyAPIError(
894
944
  {
@@ -916,18 +966,22 @@ class HomeyAPIV3 extends HomeyAPI {
916
966
  });
917
967
  };
918
968
 
969
+ const token = this.__token;
970
+
919
971
  try {
920
- const result = await Util.timeout(handshakeClient(), 5000, `Failed to handshake client (Timeout after 5000ms).`);
972
+ const result = await Util.timeout(
973
+ handshakeClient(token),
974
+ 5000,
975
+ `Failed to handshake client (Timeout after 5000ms).`
976
+ );
921
977
 
922
978
  return onResult(result);
923
979
  } catch (err) {
924
980
  if (err.statusCode === 401 || err.code === 401) {
925
981
  this.__debug('Token expired, refreshing...');
926
- await this.logout();
927
- await this.login();
928
-
982
+ await this.refreshForToken(token, false);
929
983
  const result = await Util.timeout(
930
- handshakeClient(),
984
+ handshakeClient(this.__token),
931
985
  5000,
932
986
  `Failed to handshake client (Timeout after 5000ms).`
933
987
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homey-api",
3
- "version": "3.4.19",
3
+ "version": "3.4.21",
4
4
  "description": "Homey API",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "types": "assets/types/homey-api.d.ts",
14
14
  "scripts": {
15
- "test": "jest",
15
+ "test": "jest --verbose",
16
16
  "lint": "eslint .",
17
17
  "serve": "concurrently \"serve jsdoc/\" \"npm run jsdoc:watch\"",
18
18
  "build": "npm run build:specs; npm run build:jsdoc; npm run build:types; npm run build:webpack;",
@@ -56,6 +56,7 @@
56
56
  "@babel/core": "^7.16.0",
57
57
  "@babel/plugin-proposal-class-properties": "^7.16.0",
58
58
  "@babel/preset-env": "^7.16.0",
59
+ "@types/jest": "^29.5.12",
59
60
  "@typescript-eslint/eslint-plugin": "^6.1.0",
60
61
  "@typescript-eslint/parser": "^6.1.0",
61
62
  "babel-loader": "^8.2.3",
@@ -64,17 +65,19 @@
64
65
  "eslint": "^7.32.0",
65
66
  "eslint-config-athom": "^2.1.1",
66
67
  "eslint-plugin-jest": "^27.4.0",
68
+ "express": "^4.18.2",
67
69
  "fs-extra": "^10.0.0",
68
70
  "homey-api": "^3.0.8",
69
71
  "http-server": "^0.12.3",
70
- "jest": "^29.7.0",
72
+ "jest": "^30.0.0-alpha.2",
71
73
  "jsdoc": "^4.0.2",
72
74
  "jsdoc-to-markdown": "^8.0.0",
73
75
  "jsdoc-ts-utils": "^4.0.0",
74
76
  "jsdoc-tsimport-plugin": "^1.0.5",
75
77
  "keypather": "^3.1.0",
76
- "prettier": "^3.0.0",
78
+ "prettier": "^3.2.5",
77
79
  "serve": "^14.0.1",
80
+ "socket.io": "^4.7.4",
78
81
  "typescript": "^5.1.6",
79
82
  "watch": "^1.0.2",
80
83
  "webpack": "^5.64.0",