@boozilla/homebridge-shome 1.0.2 → 1.0.4
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/dist/shomeClient.d.ts +6 -0
- package/dist/shomeClient.js +85 -45
- package/dist/shomeClient.js.map +1 -1
- package/package.json +1 -1
- package/src/shomeClient.ts +99 -43
package/dist/shomeClient.d.ts
CHANGED
|
@@ -18,8 +18,14 @@ export declare class ShomeClient {
|
|
|
18
18
|
private cachedAccessToken;
|
|
19
19
|
private ihdId;
|
|
20
20
|
private tokenExpiry;
|
|
21
|
+
private requestQueue;
|
|
22
|
+
private isProcessing;
|
|
21
23
|
constructor(log: Logger, username: string, password: string, deviceId: string);
|
|
24
|
+
private enqueue;
|
|
25
|
+
private processQueue;
|
|
26
|
+
private executeTaskWithRetries;
|
|
22
27
|
login(): Promise<string | null>;
|
|
28
|
+
private performLogin;
|
|
23
29
|
getDeviceList(): Promise<MainDevice[]>;
|
|
24
30
|
getDeviceInfo(thingId: string, type: string): Promise<SubDevice[] | null>;
|
|
25
31
|
setDevice(thingId: string, deviceId: string, type: string, controlType: string, state: string, nickname?: string): Promise<boolean>;
|
package/dist/shomeClient.js
CHANGED
|
@@ -4,6 +4,8 @@ const BASE_URL = 'https://shome-api.samsung-ihp.com';
|
|
|
4
4
|
const APP_REGST_ID = '6110736314d9eef6baf393f3e43a5342f9ccde6ef300d878385acd9264cf14d5';
|
|
5
5
|
const CHINA_APP_REGST_ID = 'SHOME==6110736314d9eef6baf393f3e43a5342f9ccde6ef300d878385acd9264cf14d5';
|
|
6
6
|
const LANGUAGE = 'KOR';
|
|
7
|
+
const MAX_RETRIES = 3;
|
|
8
|
+
const INITIAL_BACKOFF_MS = 1000;
|
|
7
9
|
export class ShomeClient {
|
|
8
10
|
log;
|
|
9
11
|
username;
|
|
@@ -12,13 +14,70 @@ export class ShomeClient {
|
|
|
12
14
|
cachedAccessToken = null;
|
|
13
15
|
ihdId = null;
|
|
14
16
|
tokenExpiry = 0;
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
requestQueue = [];
|
|
19
|
+
isProcessing = false;
|
|
15
20
|
constructor(log, username, password, deviceId) {
|
|
16
21
|
this.log = log;
|
|
17
22
|
this.username = username;
|
|
18
23
|
this.password = password;
|
|
19
24
|
this.deviceId = deviceId;
|
|
20
25
|
}
|
|
26
|
+
enqueue(request) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
this.requestQueue.push({ request, resolve, reject, authRetry: false });
|
|
29
|
+
this.processQueue();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async processQueue() {
|
|
33
|
+
if (this.isProcessing) {
|
|
34
|
+
return; // A processing loop is already running
|
|
35
|
+
}
|
|
36
|
+
this.isProcessing = true;
|
|
37
|
+
while (this.requestQueue.length > 0) {
|
|
38
|
+
const task = this.requestQueue.shift();
|
|
39
|
+
try {
|
|
40
|
+
const result = await this.executeTaskWithRetries(task);
|
|
41
|
+
task.resolve(result);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
task.reject(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.isProcessing = false;
|
|
48
|
+
}
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
async executeTaskWithRetries(task) {
|
|
51
|
+
let retries = 0;
|
|
52
|
+
while (true) {
|
|
53
|
+
try {
|
|
54
|
+
const result = await task.request();
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
|
|
59
|
+
if (isAuthError && !task.authRetry) {
|
|
60
|
+
this.log.warn('API authentication failed (401). Retrying after refreshing token.');
|
|
61
|
+
this.cachedAccessToken = null;
|
|
62
|
+
this.tokenExpiry = 0;
|
|
63
|
+
task.authRetry = true;
|
|
64
|
+
continue; // Immediately retry the request
|
|
65
|
+
}
|
|
66
|
+
if (retries >= MAX_RETRIES) {
|
|
67
|
+
this.log.error(`Request failed after ${MAX_RETRIES} retries. Giving up.`, error);
|
|
68
|
+
throw error; // Throw final error
|
|
69
|
+
}
|
|
70
|
+
retries++;
|
|
71
|
+
const backoffTime = INITIAL_BACKOFF_MS * Math.pow(2, retries - 1);
|
|
72
|
+
this.log.warn(`Request failed. Retrying in ${backoffTime}ms... (Attempt ${retries}/${MAX_RETRIES})`);
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, backoffTime));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
21
77
|
async login() {
|
|
78
|
+
return this.enqueue(() => this.performLogin());
|
|
79
|
+
}
|
|
80
|
+
async performLogin() {
|
|
22
81
|
if (!this.isTokenExpired()) {
|
|
23
82
|
return this.cachedAccessToken;
|
|
24
83
|
}
|
|
@@ -42,7 +101,6 @@ export class ShomeClient {
|
|
|
42
101
|
if (response.data && response.data.accessToken) {
|
|
43
102
|
this.cachedAccessToken = response.data.accessToken;
|
|
44
103
|
this.ihdId = response.data.ihdId;
|
|
45
|
-
// Decode token to find expiry
|
|
46
104
|
const payload = JSON.parse(Buffer.from(this.cachedAccessToken.split('.')[1], 'base64').toString());
|
|
47
105
|
this.tokenExpiry = payload.exp * 1000;
|
|
48
106
|
this.log.info('Successfully logged in to sHome API.');
|
|
@@ -55,15 +113,15 @@ export class ShomeClient {
|
|
|
55
113
|
}
|
|
56
114
|
catch (error) {
|
|
57
115
|
this.log.error(`Login error: ${error}`);
|
|
58
|
-
|
|
116
|
+
throw error;
|
|
59
117
|
}
|
|
60
118
|
}
|
|
61
119
|
async getDeviceList() {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
120
|
+
return this.enqueue(async () => {
|
|
121
|
+
const token = await this.performLogin();
|
|
122
|
+
if (!token || !this.ihdId) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
67
125
|
const createDate = this.getDateTime();
|
|
68
126
|
const hashData = this.sha512(`IHRESTAPI${this.ihdId}${createDate}`);
|
|
69
127
|
const response = await axios.get(`${BASE_URL}/v16/settings/${this.ihdId}/devices/`, {
|
|
@@ -71,18 +129,14 @@ export class ShomeClient {
|
|
|
71
129
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
72
130
|
});
|
|
73
131
|
return response.data.deviceList || [];
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
this.log.error(`Error getting device list: ${error}`);
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
132
|
+
});
|
|
79
133
|
}
|
|
80
134
|
async getDeviceInfo(thingId, type) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
135
|
+
return this.enqueue(async () => {
|
|
136
|
+
const token = await this.performLogin();
|
|
137
|
+
if (!token) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
86
140
|
const createDate = this.getDateTime();
|
|
87
141
|
const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
|
|
88
142
|
const typePath = type.toLowerCase().replace(/_/g, '');
|
|
@@ -90,19 +144,15 @@ export class ShomeClient {
|
|
|
90
144
|
params: { createDate, hashData },
|
|
91
145
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
92
146
|
});
|
|
93
|
-
return response.data.
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
this.log.error(`Error getting device info for ${thingId}: ${error}`);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
147
|
+
return response.data.deviceList || null;
|
|
148
|
+
});
|
|
99
149
|
}
|
|
100
150
|
async setDevice(thingId, deviceId, type, controlType, state, nickname) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
151
|
+
return this.enqueue(async () => {
|
|
152
|
+
const token = await this.performLogin();
|
|
153
|
+
if (!token) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
106
156
|
const createDate = this.getDateTime();
|
|
107
157
|
const hashData = this.sha512(`IHRESTAPI${thingId}${deviceId}${state}${createDate}`);
|
|
108
158
|
const typePath = type.toLowerCase().replace(/_/g, '');
|
|
@@ -118,19 +168,14 @@ export class ShomeClient {
|
|
|
118
168
|
const displayName = nickname || `${thingId}/${deviceId}`;
|
|
119
169
|
this.log.info(`[${displayName}] state set to ${state}.`);
|
|
120
170
|
return true;
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
const displayName = nickname || `${thingId}/${deviceId}`;
|
|
124
|
-
this.log.error(`Error setting device [${displayName}]: ${error}`);
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
171
|
+
});
|
|
127
172
|
}
|
|
128
173
|
async unlockDoorlock(thingId, nickname) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
174
|
+
return this.enqueue(async () => {
|
|
175
|
+
const token = await this.performLogin();
|
|
176
|
+
if (!token) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
134
179
|
const createDate = this.getDateTime();
|
|
135
180
|
const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
|
|
136
181
|
await axios.put(`${BASE_URL}/v16/settings/doorlocks/${thingId}/open-mode`, null, {
|
|
@@ -144,12 +189,7 @@ export class ShomeClient {
|
|
|
144
189
|
const displayName = nickname || thingId;
|
|
145
190
|
this.log.info(`Unlocked [${displayName}].`);
|
|
146
191
|
return true;
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
const displayName = nickname || thingId;
|
|
150
|
-
this.log.error(`Error unlocking [${displayName}]: ${error}`);
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
192
|
+
});
|
|
153
193
|
}
|
|
154
194
|
sha512(input) {
|
|
155
195
|
return CryptoJS.SHA512(input).toString();
|
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;
|
|
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;AAuBhC,MAAM,OAAO,WAAW;IASC;IACA;IACA;IACA;IAXf,iBAAiB,GAAkB,IAAI,CAAC;IACxC,KAAK,GAAkB,IAAI,CAAC;IAC5B,WAAW,GAAW,CAAC,CAAC;IAChC,8DAA8D;IACtD,YAAY,GAAqB,EAAE,CAAC;IACpC,YAAY,GAAG,KAAK,CAAC;IAE7B,YACuB,GAAW,EACX,QAAgB,EAChB,QAAgB,EAChB,QAAgB;QAHhB,QAAG,GAAH,GAAG,CAAQ;QACX,aAAQ,GAAR,QAAQ,CAAQ;QAChB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,aAAQ,GAAR,QAAQ,CAAQ;IAEvC,CAAC;IAEO,OAAO,CAAI,OAAyB;QAC1C,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;YACvE,IAAI,CAAC,YAAY,EAAE,CAAC;QACtB,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,YAAY;QACxB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,OAAO,CAAC,uCAAuC;QACjD,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAEzB,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,EAAG,CAAC;YACxC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC;gBACvD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACvB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;QAED,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;IAC5B,CAAC;IAED,8DAA8D;IACtD,KAAK,CAAC,sBAAsB,CAAC,IAAoB;QACvD,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;gBACpC,OAAO,MAAM,CAAC;YAChB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,WAAW,GAAG,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,CAAC;gBAEhF,IAAI,WAAW,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;oBACnC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAC;oBACnF,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;oBAC9B,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;oBACrB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;oBACtB,SAAS,CAAC,gCAAgC;gBAC5C,CAAC;gBAED,IAAI,OAAO,IAAI,WAAW,EAAE,CAAC;oBAC3B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,wBAAwB,WAAW,sBAAsB,EAAE,KAAK,CAAC,CAAC;oBACjF,MAAM,KAAK,CAAC,CAAC,oBAAoB;gBACnC,CAAC;gBAED,OAAO,EAAE,CAAC;gBACV,MAAM,WAAW,GAAG,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;gBAClE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,+BAA+B,WAAW,kBAAkB,OAAO,IAAI,WAAW,GAAG,CAAC,CAAC;gBACrG,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;YACjE,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;IACjD,CAAC;IAEO,KAAK,CAAC,YAAY;QACxB,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC,iBAAiB,CAAC;QAChC,CAAC;QAED,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACtC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,QAAQ,GAAG,cAAc,GAAG,IAAI,CAAC,QAAQ,EAAE;gBAC/E,GAAG,YAAY,GAAG,kBAAkB,GAAG,QAAQ,GAAG,UAAU,EAAE,CAAC,CAAC;YAE1E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,QAAQ,kBAAkB,EAAE,IAAI,EAAE;gBACpE,MAAM,EAAE;oBACN,UAAU,EAAE,YAAY;oBACxB,eAAe,EAAE,kBAAkB;oBACnC,UAAU,EAAE,UAAU;oBACtB,QAAQ,EAAE,QAAQ;oBAClB,QAAQ,EAAE,QAAQ;oBAClB,gBAAgB,EAAE,IAAI,CAAC,QAAQ;oBAC/B,QAAQ,EAAE,cAAc;oBACxB,MAAM,EAAE,IAAI,CAAC,QAAQ;iBACtB;aACF,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;gBAC/C,IAAI,CAAC,iBAAiB,GAAG,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC;gBACnD,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;gBAEjC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAkB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;gBACpG,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;gBAEtC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;gBACtD,OAAO,IAAI,CAAC,iBAAiB,CAAC;YAChC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;gBAClE,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,KAAK,EAAE,CAAC,CAAC;YACxC,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE;YAC7B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;YACxC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC1B,OAAO,EAAE,CAAC;YACZ,CAAC;YAED,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,KAAK,GAAG,UAAU,EAAE,CAAC,CAAC;YAEpE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,QAAQ,iBAAiB,IAAI,CAAC,KAAK,WAAW,EAAE;gBAClF,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE;gBAChC,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,KAAK,EAAE,EAAE;aAChD,CAAC,CAAC;YACH,OAAO,QAAQ,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAe,EAAE,IAAY;QAC/C,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE;YAC7B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;YACxC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,IAAI,CAAC;YACd,CAAC;YAED,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,OAAO,GAAG,UAAU,EAAE,CAAC,CAAC;YACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAEtD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,QAAQ,iBAAiB,QAAQ,IAAI,OAAO,EAAE,EAAE;gBAClF,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE;gBAChC,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,KAAK,EAAE,EAAE;aAChD,CAAC,CAAC;YACH,OAAO,QAAQ,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC;QAC1C,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,OAAe,EAAE,QAAgB,EAAE,IAAY,EAAE,WAAmB,EAAE,KAAa,EAAE,QAAiB;QACpH,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE;YAC7B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;YACxC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,KAAK,CAAC;YACf,CAAC;YAED,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,UAAU,EAAE,CAAC,CAAC;YACpF,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACtD,MAAM,WAAW,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAEjE,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,QAAQ,iBAAiB,QAAQ,IAAI,OAAO,IAAI,QAAQ,IAAI,WAAW,EAAE,EAAE,IAAI,EAAE;gBAClG,MAAM,EAAE;oBACN,UAAU;oBACV,CAAC,WAAW,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK;oBACvD,QAAQ;iBACT;gBACD,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,KAAK,EAAE,EAAE;aAChD,CAAC,CAAC;YAEH,MAAM,WAAW,GAAG,QAAQ,IAAI,GAAG,OAAO,IAAI,QAAQ,EAAE,CAAC;YACzD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,WAAW,kBAAkB,KAAK,GAAG,CAAC,CAAC;YACzD,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,OAAe,EAAE,QAAiB;QACrD,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE;YAC7B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;YACxC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,KAAK,CAAC;YACf,CAAC;YAED,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,OAAO,GAAG,UAAU,EAAE,CAAC,CAAC;YAEjE,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,QAAQ,2BAA2B,OAAO,YAAY,EAAE,IAAI,EAAE;gBAC/E,MAAM,EAAE;oBACN,UAAU;oBACV,GAAG,EAAE,EAAE;oBACP,QAAQ;iBACT;gBACD,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,KAAK,EAAE,EAAE;aAChD,CAAC,CAAC;YAEH,MAAM,WAAW,GAAG,QAAQ,IAAI,OAAO,CAAC;YACxC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,WAAW,IAAI,CAAC,CAAC;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,MAAM,CAAC,KAAa;QAC1B,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC3C,CAAC;IAEO,cAAc;QACpB,OAAO,CAAC,IAAI,CAAC,iBAAiB,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC;IACnE,CAAC;IAEO,WAAW;QACjB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAC7D,OAAO,GAAG,GAAG,CAAC,cAAc,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,EAAE;YAC7E,GAAG,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,EAAE,CAAC;IAC5F,CAAC;CACF"}
|
package/package.json
CHANGED
package/src/shomeClient.ts
CHANGED
|
@@ -6,6 +6,8 @@ const BASE_URL = 'https://shome-api.samsung-ihp.com';
|
|
|
6
6
|
const APP_REGST_ID = '6110736314d9eef6baf393f3e43a5342f9ccde6ef300d878385acd9264cf14d5';
|
|
7
7
|
const CHINA_APP_REGST_ID = 'SHOME==6110736314d9eef6baf393f3e43a5342f9ccde6ef300d878385acd9264cf14d5';
|
|
8
8
|
const LANGUAGE = 'KOR';
|
|
9
|
+
const MAX_RETRIES = 3;
|
|
10
|
+
const INITIAL_BACKOFF_MS = 1000;
|
|
9
11
|
|
|
10
12
|
// Define and export interfaces for device types
|
|
11
13
|
export interface MainDevice {
|
|
@@ -21,10 +23,20 @@ export interface SubDevice {
|
|
|
21
23
|
[key: string]: unknown;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
type QueueTask<T = unknown> = {
|
|
27
|
+
request: () => Promise<T>;
|
|
28
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
29
|
+
reject: (reason?: unknown) => void;
|
|
30
|
+
authRetry: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
24
33
|
export class ShomeClient {
|
|
25
34
|
private cachedAccessToken: string | null = null;
|
|
26
35
|
private ihdId: string | null = null;
|
|
27
36
|
private tokenExpiry: number = 0;
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
private requestQueue: QueueTask<any>[] = [];
|
|
39
|
+
private isProcessing = false;
|
|
28
40
|
|
|
29
41
|
constructor(
|
|
30
42
|
private readonly log: Logger,
|
|
@@ -34,7 +46,68 @@ export class ShomeClient {
|
|
|
34
46
|
) {
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
private enqueue<T>(request: () => Promise<T>): Promise<T> {
|
|
50
|
+
return new Promise<T>((resolve, reject) => {
|
|
51
|
+
this.requestQueue.push({ request, resolve, reject, authRetry: false });
|
|
52
|
+
this.processQueue();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async processQueue(): Promise<void> {
|
|
57
|
+
if (this.isProcessing) {
|
|
58
|
+
return; // A processing loop is already running
|
|
59
|
+
}
|
|
60
|
+
this.isProcessing = true;
|
|
61
|
+
|
|
62
|
+
while (this.requestQueue.length > 0) {
|
|
63
|
+
const task = this.requestQueue.shift()!;
|
|
64
|
+
try {
|
|
65
|
+
const result = await this.executeTaskWithRetries(task);
|
|
66
|
+
task.resolve(result);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
task.reject(error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.isProcessing = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
private async executeTaskWithRetries(task: QueueTask<any>): Promise<any> {
|
|
77
|
+
let retries = 0;
|
|
78
|
+
while (true) {
|
|
79
|
+
try {
|
|
80
|
+
const result = await task.request();
|
|
81
|
+
return result;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
|
|
84
|
+
|
|
85
|
+
if (isAuthError && !task.authRetry) {
|
|
86
|
+
this.log.warn('API authentication failed (401). Retrying after refreshing token.');
|
|
87
|
+
this.cachedAccessToken = null;
|
|
88
|
+
this.tokenExpiry = 0;
|
|
89
|
+
task.authRetry = true;
|
|
90
|
+
continue; // Immediately retry the request
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (retries >= MAX_RETRIES) {
|
|
94
|
+
this.log.error(`Request failed after ${MAX_RETRIES} retries. Giving up.`, error);
|
|
95
|
+
throw error; // Throw final error
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
retries++;
|
|
99
|
+
const backoffTime = INITIAL_BACKOFF_MS * Math.pow(2, retries - 1);
|
|
100
|
+
this.log.warn(`Request failed. Retrying in ${backoffTime}ms... (Attempt ${retries}/${MAX_RETRIES})`);
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, backoffTime));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
37
106
|
async login(): Promise<string | null> {
|
|
107
|
+
return this.enqueue(() => this.performLogin());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async performLogin(): Promise<string | null> {
|
|
38
111
|
if (!this.isTokenExpired()) {
|
|
39
112
|
return this.cachedAccessToken;
|
|
40
113
|
}
|
|
@@ -62,7 +135,6 @@ export class ShomeClient {
|
|
|
62
135
|
this.cachedAccessToken = response.data.accessToken;
|
|
63
136
|
this.ihdId = response.data.ihdId;
|
|
64
137
|
|
|
65
|
-
// Decode token to find expiry
|
|
66
138
|
const payload = JSON.parse(Buffer.from(this.cachedAccessToken!.split('.')[1], 'base64').toString());
|
|
67
139
|
this.tokenExpiry = payload.exp * 1000;
|
|
68
140
|
|
|
@@ -74,17 +146,17 @@ export class ShomeClient {
|
|
|
74
146
|
}
|
|
75
147
|
} catch (error) {
|
|
76
148
|
this.log.error(`Login error: ${error}`);
|
|
77
|
-
|
|
149
|
+
throw error;
|
|
78
150
|
}
|
|
79
151
|
}
|
|
80
152
|
|
|
81
153
|
async getDeviceList(): Promise<MainDevice[]> {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
154
|
+
return this.enqueue(async () => {
|
|
155
|
+
const token = await this.performLogin();
|
|
156
|
+
if (!token || !this.ihdId) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
86
159
|
|
|
87
|
-
try {
|
|
88
160
|
const createDate = this.getDateTime();
|
|
89
161
|
const hashData = this.sha512(`IHRESTAPI${this.ihdId}${createDate}`);
|
|
90
162
|
|
|
@@ -92,21 +164,17 @@ export class ShomeClient {
|
|
|
92
164
|
params: { createDate, hashData },
|
|
93
165
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
94
166
|
});
|
|
95
|
-
|
|
96
167
|
return response.data.deviceList || [];
|
|
97
|
-
}
|
|
98
|
-
this.log.error(`Error getting device list: ${error}`);
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
168
|
+
});
|
|
101
169
|
}
|
|
102
170
|
|
|
103
171
|
async getDeviceInfo(thingId: string, type: string): Promise<SubDevice[] | null> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
172
|
+
return this.enqueue(async () => {
|
|
173
|
+
const token = await this.performLogin();
|
|
174
|
+
if (!token) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
108
177
|
|
|
109
|
-
try {
|
|
110
178
|
const createDate = this.getDateTime();
|
|
111
179
|
const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
|
|
112
180
|
const typePath = type.toLowerCase().replace(/_/g, '');
|
|
@@ -115,21 +183,17 @@ export class ShomeClient {
|
|
|
115
183
|
params: { createDate, hashData },
|
|
116
184
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
117
185
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
} catch (error) {
|
|
121
|
-
this.log.error(`Error getting device info for ${thingId}: ${error}`);
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
186
|
+
return response.data.deviceList || null;
|
|
187
|
+
});
|
|
124
188
|
}
|
|
125
189
|
|
|
126
190
|
async setDevice(thingId: string, deviceId: string, type: string, controlType: string, state: string, nickname?: string): Promise<boolean> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
191
|
+
return this.enqueue(async () => {
|
|
192
|
+
const token = await this.performLogin();
|
|
193
|
+
if (!token) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
131
196
|
|
|
132
|
-
try {
|
|
133
197
|
const createDate = this.getDateTime();
|
|
134
198
|
const hashData = this.sha512(`IHRESTAPI${thingId}${deviceId}${state}${createDate}`);
|
|
135
199
|
const typePath = type.toLowerCase().replace(/_/g, '');
|
|
@@ -147,20 +211,16 @@ export class ShomeClient {
|
|
|
147
211
|
const displayName = nickname || `${thingId}/${deviceId}`;
|
|
148
212
|
this.log.info(`[${displayName}] state set to ${state}.`);
|
|
149
213
|
return true;
|
|
150
|
-
}
|
|
151
|
-
const displayName = nickname || `${thingId}/${deviceId}`;
|
|
152
|
-
this.log.error(`Error setting device [${displayName}]: ${error}`);
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
214
|
+
});
|
|
155
215
|
}
|
|
156
216
|
|
|
157
217
|
async unlockDoorlock(thingId: string, nickname?: string): Promise<boolean> {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
218
|
+
return this.enqueue(async () => {
|
|
219
|
+
const token = await this.performLogin();
|
|
220
|
+
if (!token) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
162
223
|
|
|
163
|
-
try {
|
|
164
224
|
const createDate = this.getDateTime();
|
|
165
225
|
const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
|
|
166
226
|
|
|
@@ -176,11 +236,7 @@ export class ShomeClient {
|
|
|
176
236
|
const displayName = nickname || thingId;
|
|
177
237
|
this.log.info(`Unlocked [${displayName}].`);
|
|
178
238
|
return true;
|
|
179
|
-
}
|
|
180
|
-
const displayName = nickname || thingId;
|
|
181
|
-
this.log.error(`Error unlocking [${displayName}]: ${error}`);
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
239
|
+
});
|
|
184
240
|
}
|
|
185
241
|
|
|
186
242
|
private sha512(input: string): string {
|