@boozilla/homebridge-shome 1.0.10 → 1.0.12
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/README.md +10 -8
- package/config.schema.json +7 -11
- package/dist/shomeClient.d.ts +7 -6
- package/dist/shomeClient.js +86 -63
- package/dist/shomeClient.js.map +1 -1
- package/package.json +1 -1
- package/src/shomeClient.ts +93 -66
package/README.md
CHANGED
|
@@ -36,7 +36,8 @@ Add a new platform to the `platforms` array in your Homebridge `config.json` fil
|
|
|
36
36
|
"name": "sHome",
|
|
37
37
|
"username": "YOUR_SHOME_USERNAME",
|
|
38
38
|
"password": "YOUR_SHOME_PASSWORD",
|
|
39
|
-
"deviceId": "YOUR_MOBILE_DEVICE_ID"
|
|
39
|
+
"deviceId": "YOUR_MOBILE_DEVICE_ID",
|
|
40
|
+
"pollingInterval": 3000
|
|
40
41
|
}
|
|
41
42
|
]
|
|
42
43
|
}
|
|
@@ -44,13 +45,14 @@ Add a new platform to the `platforms` array in your Homebridge `config.json` fil
|
|
|
44
45
|
|
|
45
46
|
### Configuration Fields
|
|
46
47
|
|
|
47
|
-
| Key
|
|
48
|
-
|
|
49
|
-
| `platform`
|
|
50
|
-
| `name`
|
|
51
|
-
| `username`
|
|
52
|
-
| `password`
|
|
53
|
-
| `deviceId`
|
|
48
|
+
| Key | Description | Required |
|
|
49
|
+
|:------------------|:--------------------------------------------------------------------------------------------------------------|:---------|
|
|
50
|
+
| `platform` | Must be set to **"sHome"**. | Yes |
|
|
51
|
+
| `name` | The name of the platform that will appear in the Homebridge logs (e.g., "sHome"). | Yes |
|
|
52
|
+
| `username` | Your Samsung sHome account username. | Yes |
|
|
53
|
+
| `password` | Your Samsung sHome account password. | Yes |
|
|
54
|
+
| `deviceId` | The unique ID of the mobile device where the sHome app is installed. This is required for API authentication. | Yes |
|
|
55
|
+
| `pollingInterval` | The interval in milliseconds to poll for device status updates. Set to 0 to disable. | No |
|
|
54
56
|
|
|
55
57
|
**Note:** The `deviceId` can often be found by inspecting the sHome app's internal storage or by using specific tools to
|
|
56
58
|
retrieve it. A correct value is essential for the API login to succeed.
|
package/config.schema.json
CHANGED
|
@@ -5,36 +5,32 @@
|
|
|
5
5
|
"name": {
|
|
6
6
|
"title": "Name",
|
|
7
7
|
"type": "string",
|
|
8
|
-
"default": "sHome"
|
|
9
|
-
"required": true
|
|
8
|
+
"default": "sHome"
|
|
10
9
|
},
|
|
11
10
|
"username": {
|
|
12
11
|
"title": "Username",
|
|
13
12
|
"type": "string",
|
|
14
|
-
"description": "Your Samsung sHome account username."
|
|
15
|
-
"required": true
|
|
13
|
+
"description": "Your Samsung sHome account username."
|
|
16
14
|
},
|
|
17
15
|
"password": {
|
|
18
16
|
"title": "Password",
|
|
19
17
|
"type": "string",
|
|
20
18
|
"description": "Your Samsung sHome account password.",
|
|
21
|
-
"format": "password"
|
|
22
|
-
"required": true
|
|
19
|
+
"format": "password"
|
|
23
20
|
},
|
|
24
21
|
"deviceId": {
|
|
25
22
|
"title": "Device ID",
|
|
26
23
|
"type": "string",
|
|
27
|
-
"description": "Your mobile device ID."
|
|
28
|
-
"required": true
|
|
24
|
+
"description": "Your mobile device ID."
|
|
29
25
|
},
|
|
30
26
|
"pollingInterval": {
|
|
31
27
|
"title": "Polling Interval (ms)",
|
|
32
28
|
"type": "integer",
|
|
33
29
|
"default": 3000,
|
|
34
|
-
"description": "The interval in milliseconds to poll for device status updates. Set to 0 to disable."
|
|
35
|
-
"required": false
|
|
30
|
+
"description": "The interval in milliseconds to poll for device status updates. Set to 0 to disable."
|
|
36
31
|
}
|
|
37
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"required": ["name", "username", "password", "deviceId"]
|
|
38
34
|
},
|
|
39
35
|
"pluginAlias": "sHome",
|
|
40
36
|
"pluginType": "platform"
|
package/dist/shomeClient.d.ts
CHANGED
|
@@ -18,14 +18,15 @@ export declare class ShomeClient {
|
|
|
18
18
|
private cachedAccessToken;
|
|
19
19
|
private ihdId;
|
|
20
20
|
private tokenExpiry;
|
|
21
|
-
private
|
|
22
|
-
private
|
|
21
|
+
private putQueue;
|
|
22
|
+
private isProcessingPut;
|
|
23
|
+
private loginPromise;
|
|
23
24
|
constructor(log: Logger, username: string, password: string, deviceId: string);
|
|
24
|
-
private
|
|
25
|
-
private processQueue;
|
|
26
|
-
private executeTaskWithRetries;
|
|
27
|
-
login(): Promise<string | null>;
|
|
25
|
+
private login;
|
|
28
26
|
private performLogin;
|
|
27
|
+
private enqueuePut;
|
|
28
|
+
private processPutQueue;
|
|
29
|
+
private executeWithRetries;
|
|
29
30
|
getDeviceList(): Promise<MainDevice[]>;
|
|
30
31
|
getDeviceInfo(thingId: string, type: string): Promise<SubDevice[] | null>;
|
|
31
32
|
setDevice(thingId: string, deviceId: string, type: string, controlType: string, state: string, nickname?: string): Promise<boolean>;
|
package/dist/shomeClient.js
CHANGED
|
@@ -16,69 +16,35 @@ export class ShomeClient {
|
|
|
16
16
|
ihdId = null;
|
|
17
17
|
tokenExpiry = 0;
|
|
18
18
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
putQueue = [];
|
|
20
|
+
isProcessingPut = false;
|
|
21
|
+
loginPromise = null;
|
|
21
22
|
constructor(log, username, password, deviceId) {
|
|
22
23
|
this.log = log;
|
|
23
24
|
this.username = username;
|
|
24
25
|
this.password = password;
|
|
25
26
|
this.deviceId = deviceId;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.
|
|
30
|
-
this.processQueue();
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
async processQueue() {
|
|
34
|
-
if (this.isProcessing) {
|
|
35
|
-
return; // A processing loop is already running
|
|
36
|
-
}
|
|
37
|
-
this.isProcessing = true;
|
|
38
|
-
while (this.requestQueue.length > 0) {
|
|
39
|
-
const task = this.requestQueue.shift();
|
|
40
|
-
try {
|
|
41
|
-
const result = await this.executeTaskWithRetries(task);
|
|
42
|
-
task.resolve(result);
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
task.reject(error);
|
|
46
|
-
}
|
|
47
|
-
// Add a delay between requests to avoid overwhelming the server
|
|
48
|
-
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
28
|
+
login() {
|
|
29
|
+
if (!this.isTokenExpired()) {
|
|
30
|
+
return Promise.resolve(this.cachedAccessToken);
|
|
49
31
|
}
|
|
50
|
-
this.
|
|
51
|
-
|
|
52
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
-
async executeTaskWithRetries(task) {
|
|
54
|
-
let retries = 0;
|
|
55
|
-
while (true) {
|
|
56
|
-
try {
|
|
57
|
-
const result = await task.request();
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
|
|
62
|
-
if (isAuthError && !task.authRetry) {
|
|
63
|
-
this.log.warn('API authentication failed (401). Retrying after refreshing token.');
|
|
64
|
-
this.cachedAccessToken = null;
|
|
65
|
-
this.tokenExpiry = 0;
|
|
66
|
-
task.authRetry = true;
|
|
67
|
-
continue; // Immediately retry the request
|
|
68
|
-
}
|
|
69
|
-
if (retries >= MAX_RETRIES) {
|
|
70
|
-
this.log.error(`Request failed after ${MAX_RETRIES} retries. Giving up.`, error);
|
|
71
|
-
throw error; // Throw final error
|
|
72
|
-
}
|
|
73
|
-
retries++;
|
|
74
|
-
const backoffTime = INITIAL_BACKOFF_MS * Math.pow(2, retries - 1);
|
|
75
|
-
this.log.warn(`Request failed. Retrying in ${backoffTime}ms... (Attempt ${retries}/${MAX_RETRIES})`);
|
|
76
|
-
await new Promise(resolve => setTimeout(resolve, backoffTime));
|
|
77
|
-
}
|
|
32
|
+
if (this.loginPromise) {
|
|
33
|
+
return this.loginPromise;
|
|
78
34
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
35
|
+
this.loginPromise = new Promise((resolve, reject) => {
|
|
36
|
+
this.putQueue.unshift({
|
|
37
|
+
request: () => this.performLogin(),
|
|
38
|
+
resolve,
|
|
39
|
+
reject,
|
|
40
|
+
});
|
|
41
|
+
this.processPutQueue();
|
|
42
|
+
});
|
|
43
|
+
// Clean up the promise once it's settled
|
|
44
|
+
this.loginPromise.finally(() => {
|
|
45
|
+
this.loginPromise = null;
|
|
46
|
+
});
|
|
47
|
+
return this.loginPromise;
|
|
82
48
|
}
|
|
83
49
|
async performLogin() {
|
|
84
50
|
if (!this.isTokenExpired()) {
|
|
@@ -119,9 +85,66 @@ export class ShomeClient {
|
|
|
119
85
|
throw error;
|
|
120
86
|
}
|
|
121
87
|
}
|
|
88
|
+
enqueuePut(request) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
this.putQueue.push({ request, resolve, reject });
|
|
91
|
+
this.processPutQueue();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async processPutQueue() {
|
|
95
|
+
if (this.isProcessingPut) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.isProcessingPut = true;
|
|
99
|
+
while (this.putQueue.length > 0) {
|
|
100
|
+
const task = this.putQueue.shift();
|
|
101
|
+
try {
|
|
102
|
+
const result = await this.executeWithRetries(task.request, true);
|
|
103
|
+
task.resolve(result);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
task.reject(error);
|
|
107
|
+
}
|
|
108
|
+
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
109
|
+
}
|
|
110
|
+
this.isProcessingPut = false;
|
|
111
|
+
}
|
|
112
|
+
async executeWithRetries(request, isQueued = false) {
|
|
113
|
+
let retries = 0;
|
|
114
|
+
while (true) {
|
|
115
|
+
try {
|
|
116
|
+
// For non-queued (concurrent) requests, we need to ensure login happens before the request.
|
|
117
|
+
// For queued requests, the login is handled as part of the queue, so we can just await it.
|
|
118
|
+
if (!isQueued) {
|
|
119
|
+
await this.login();
|
|
120
|
+
}
|
|
121
|
+
return await request();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
|
|
125
|
+
if (isAuthError) {
|
|
126
|
+
this.log.warn('API authentication failed (401). Invalidating token.');
|
|
127
|
+
this.cachedAccessToken = null;
|
|
128
|
+
this.tokenExpiry = 0;
|
|
129
|
+
}
|
|
130
|
+
if (retries >= MAX_RETRIES) {
|
|
131
|
+
this.log.error(`Request failed after ${MAX_RETRIES} retries. Giving up.`, error);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
retries++;
|
|
135
|
+
const backoffTime = INITIAL_BACKOFF_MS * Math.pow(2, retries - 1);
|
|
136
|
+
if (!isAuthError) {
|
|
137
|
+
this.log.warn(`Request failed. Retrying in ${backoffTime}ms... (Attempt ${retries}/${MAX_RETRIES})`);
|
|
138
|
+
}
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, backoffTime));
|
|
140
|
+
// After a failure, always ensure we are logged in before the next attempt.
|
|
141
|
+
await this.login();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
122
145
|
async getDeviceList() {
|
|
123
|
-
return this.
|
|
124
|
-
const token =
|
|
146
|
+
return this.executeWithRetries(async () => {
|
|
147
|
+
const token = this.cachedAccessToken;
|
|
125
148
|
if (!token || !this.ihdId) {
|
|
126
149
|
return [];
|
|
127
150
|
}
|
|
@@ -135,8 +158,8 @@ export class ShomeClient {
|
|
|
135
158
|
});
|
|
136
159
|
}
|
|
137
160
|
async getDeviceInfo(thingId, type) {
|
|
138
|
-
return this.
|
|
139
|
-
const token =
|
|
161
|
+
return this.executeWithRetries(async () => {
|
|
162
|
+
const token = this.cachedAccessToken;
|
|
140
163
|
if (!token) {
|
|
141
164
|
return null;
|
|
142
165
|
}
|
|
@@ -151,8 +174,8 @@ export class ShomeClient {
|
|
|
151
174
|
});
|
|
152
175
|
}
|
|
153
176
|
async setDevice(thingId, deviceId, type, controlType, state, nickname) {
|
|
154
|
-
return this.
|
|
155
|
-
const token =
|
|
177
|
+
return this.enqueuePut(async () => {
|
|
178
|
+
const token = this.cachedAccessToken;
|
|
156
179
|
if (!token) {
|
|
157
180
|
return false;
|
|
158
181
|
}
|
|
@@ -174,8 +197,8 @@ export class ShomeClient {
|
|
|
174
197
|
});
|
|
175
198
|
}
|
|
176
199
|
async unlockDoorlock(thingId, nickname) {
|
|
177
|
-
return this.
|
|
178
|
-
const token =
|
|
200
|
+
return this.enqueuePut(async () => {
|
|
201
|
+
const token = this.cachedAccessToken;
|
|
179
202
|
if (!token) {
|
|
180
203
|
return false;
|
|
181
204
|
}
|
package/dist/shomeClient.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shomeClient.js","sourceRoot":"","sources":["../src/shomeClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,QAAQ,MAAM,WAAW,CAAC;AAGjC,MAAM,QAAQ,GAAG,mCAAmC,CAAC;AACrD,MAAM,YAAY,GAAG,kEAAkE,CAAC;AACxF,MAAM,kBAAkB,GAAG,yEAAyE,CAAC;AACrG,MAAM,QAAQ,GAAG,KAAK,CAAC;AACvB,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAChC,MAAM,gBAAgB,GAAG,GAAG,CAAC,CAAC,wBAAwB;
|
|
1
|
+
{"version":3,"file":"shomeClient.js","sourceRoot":"","sources":["../src/shomeClient.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/shomeClient.ts
CHANGED
|
@@ -28,7 +28,6 @@ type QueueTask<T = unknown> = {
|
|
|
28
28
|
request: () => Promise<T>;
|
|
29
29
|
resolve: (value: T | PromiseLike<T>) => void;
|
|
30
30
|
reject: (reason?: unknown) => void;
|
|
31
|
-
authRetry: boolean;
|
|
32
31
|
};
|
|
33
32
|
|
|
34
33
|
export class ShomeClient {
|
|
@@ -36,8 +35,9 @@ export class ShomeClient {
|
|
|
36
35
|
private ihdId: string | null = null;
|
|
37
36
|
private tokenExpiry: number = 0;
|
|
38
37
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
-
private
|
|
40
|
-
private
|
|
38
|
+
private putQueue: QueueTask<any>[] = [];
|
|
39
|
+
private isProcessingPut = false;
|
|
40
|
+
private loginPromise: Promise<string | null> | null = null;
|
|
41
41
|
|
|
42
42
|
constructor(
|
|
43
43
|
private readonly log: Logger,
|
|
@@ -47,67 +47,30 @@ export class ShomeClient {
|
|
|
47
47
|
) {
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
this.processQueue();
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private async processQueue(): Promise<void> {
|
|
58
|
-
if (this.isProcessing) {
|
|
59
|
-
return; // A processing loop is already running
|
|
50
|
+
private login(): Promise<string | null> {
|
|
51
|
+
if (!this.isTokenExpired()) {
|
|
52
|
+
return Promise.resolve(this.cachedAccessToken);
|
|
60
53
|
}
|
|
61
|
-
this.isProcessing = true;
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const result = await this.executeTaskWithRetries(task);
|
|
67
|
-
task.resolve(result);
|
|
68
|
-
} catch (error) {
|
|
69
|
-
task.reject(error);
|
|
70
|
-
}
|
|
71
|
-
// Add a delay between requests to avoid overwhelming the server
|
|
72
|
-
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
55
|
+
if (this.loginPromise) {
|
|
56
|
+
return this.loginPromise;
|
|
73
57
|
}
|
|
74
58
|
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const result = await task.request();
|
|
84
|
-
return result;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
|
|
87
|
-
|
|
88
|
-
if (isAuthError && !task.authRetry) {
|
|
89
|
-
this.log.warn('API authentication failed (401). Retrying after refreshing token.');
|
|
90
|
-
this.cachedAccessToken = null;
|
|
91
|
-
this.tokenExpiry = 0;
|
|
92
|
-
task.authRetry = true;
|
|
93
|
-
continue; // Immediately retry the request
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (retries >= MAX_RETRIES) {
|
|
97
|
-
this.log.error(`Request failed after ${MAX_RETRIES} retries. Giving up.`, error);
|
|
98
|
-
throw error; // Throw final error
|
|
99
|
-
}
|
|
59
|
+
this.loginPromise = new Promise((resolve, reject) => {
|
|
60
|
+
this.putQueue.unshift({ // Prioritize login by adding to the front of the queue
|
|
61
|
+
request: () => this.performLogin(),
|
|
62
|
+
resolve,
|
|
63
|
+
reject,
|
|
64
|
+
});
|
|
65
|
+
this.processPutQueue();
|
|
66
|
+
});
|
|
100
67
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
68
|
+
// Clean up the promise once it's settled
|
|
69
|
+
this.loginPromise.finally(() => {
|
|
70
|
+
this.loginPromise = null;
|
|
71
|
+
});
|
|
108
72
|
|
|
109
|
-
|
|
110
|
-
return this.enqueue(() => this.performLogin());
|
|
73
|
+
return this.loginPromise;
|
|
111
74
|
}
|
|
112
75
|
|
|
113
76
|
private async performLogin(): Promise<string | null> {
|
|
@@ -153,9 +116,73 @@ export class ShomeClient {
|
|
|
153
116
|
}
|
|
154
117
|
}
|
|
155
118
|
|
|
119
|
+
private enqueuePut<T>(request: () => Promise<T>): Promise<T> {
|
|
120
|
+
return new Promise<T>((resolve, reject) => {
|
|
121
|
+
this.putQueue.push({ request, resolve, reject });
|
|
122
|
+
this.processPutQueue();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async processPutQueue(): Promise<void> {
|
|
127
|
+
if (this.isProcessingPut) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.isProcessingPut = true;
|
|
131
|
+
|
|
132
|
+
while (this.putQueue.length > 0) {
|
|
133
|
+
const task = this.putQueue.shift()!;
|
|
134
|
+
try {
|
|
135
|
+
const result = await this.executeWithRetries(task.request, true);
|
|
136
|
+
task.resolve(result);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
task.reject(error);
|
|
139
|
+
}
|
|
140
|
+
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.isProcessingPut = false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async executeWithRetries<T>(request: () => Promise<T>, isQueued = false): Promise<T> {
|
|
147
|
+
let retries = 0;
|
|
148
|
+
while (true) {
|
|
149
|
+
try {
|
|
150
|
+
// For non-queued (concurrent) requests, we need to ensure login happens before the request.
|
|
151
|
+
// For queued requests, the login is handled as part of the queue, so we can just await it.
|
|
152
|
+
if (!isQueued) {
|
|
153
|
+
await this.login();
|
|
154
|
+
}
|
|
155
|
+
return await request();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
|
|
158
|
+
|
|
159
|
+
if (isAuthError) {
|
|
160
|
+
this.log.warn('API authentication failed (401). Invalidating token.');
|
|
161
|
+
this.cachedAccessToken = null;
|
|
162
|
+
this.tokenExpiry = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (retries >= MAX_RETRIES) {
|
|
166
|
+
this.log.error(`Request failed after ${MAX_RETRIES} retries. Giving up.`, error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
retries++;
|
|
171
|
+
const backoffTime = INITIAL_BACKOFF_MS * Math.pow(2, retries - 1);
|
|
172
|
+
if (!isAuthError) {
|
|
173
|
+
this.log.warn(`Request failed. Retrying in ${backoffTime}ms... (Attempt ${retries}/${MAX_RETRIES})`);
|
|
174
|
+
}
|
|
175
|
+
await new Promise(resolve => setTimeout(resolve, backoffTime));
|
|
176
|
+
|
|
177
|
+
// After a failure, always ensure we are logged in before the next attempt.
|
|
178
|
+
await this.login();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
156
183
|
async getDeviceList(): Promise<MainDevice[]> {
|
|
157
|
-
return this.
|
|
158
|
-
const token =
|
|
184
|
+
return this.executeWithRetries(async () => {
|
|
185
|
+
const token = this.cachedAccessToken;
|
|
159
186
|
if (!token || !this.ihdId) {
|
|
160
187
|
return [];
|
|
161
188
|
}
|
|
@@ -173,8 +200,8 @@ export class ShomeClient {
|
|
|
173
200
|
}
|
|
174
201
|
|
|
175
202
|
async getDeviceInfo(thingId: string, type: string): Promise<SubDevice[] | null> {
|
|
176
|
-
return this.
|
|
177
|
-
const token =
|
|
203
|
+
return this.executeWithRetries(async () => {
|
|
204
|
+
const token = this.cachedAccessToken;
|
|
178
205
|
if (!token) {
|
|
179
206
|
return null;
|
|
180
207
|
}
|
|
@@ -193,8 +220,8 @@ export class ShomeClient {
|
|
|
193
220
|
}
|
|
194
221
|
|
|
195
222
|
async setDevice(thingId: string, deviceId: string, type: string, controlType: string, state: string, nickname?: string): Promise<boolean> {
|
|
196
|
-
return this.
|
|
197
|
-
const token =
|
|
223
|
+
return this.enqueuePut(async () => {
|
|
224
|
+
const token = this.cachedAccessToken;
|
|
198
225
|
if (!token) {
|
|
199
226
|
return false;
|
|
200
227
|
}
|
|
@@ -220,8 +247,8 @@ export class ShomeClient {
|
|
|
220
247
|
}
|
|
221
248
|
|
|
222
249
|
async unlockDoorlock(thingId: string, nickname?: string): Promise<boolean> {
|
|
223
|
-
return this.
|
|
224
|
-
const token =
|
|
250
|
+
return this.enqueuePut(async () => {
|
|
251
|
+
const token = this.cachedAccessToken;
|
|
225
252
|
if (!token) {
|
|
226
253
|
return false;
|
|
227
254
|
}
|