@boozilla/homebridge-shome 1.0.1 → 1.0.3
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/config.schema.json +13 -9
- package/dist/shomeClient.d.ts +5 -0
- package/dist/shomeClient.js +80 -44
- package/dist/shomeClient.js.map +1 -1
- package/package.json +1 -1
- package/src/shomeClient.ts +90 -42
package/config.schema.json
CHANGED
|
@@ -5,28 +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
|
-
"required": true,
|
|
22
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
|
-
}
|
|
31
|
-
|
|
26
|
+
},
|
|
27
|
+
"required": [
|
|
28
|
+
"name",
|
|
29
|
+
"username",
|
|
30
|
+
"password",
|
|
31
|
+
"deviceId"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"pluginAlias": "sHome",
|
|
35
|
+
"pluginType": "platform"
|
|
32
36
|
}
|
package/dist/shomeClient.d.ts
CHANGED
|
@@ -18,8 +18,13 @@ 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;
|
|
22
26
|
login(): Promise<string | null>;
|
|
27
|
+
private performLogin;
|
|
23
28
|
getDeviceList(): Promise<MainDevice[]>;
|
|
24
29
|
getDeviceInfo(thingId: string, type: string): Promise<SubDevice[] | null>;
|
|
25
30
|
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,66 @@ 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
|
+
async enqueue(request) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
this.requestQueue.push({ request, resolve, reject, authRetry: false });
|
|
29
|
+
if (!this.isProcessing) {
|
|
30
|
+
this.processQueue();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async processQueue() {
|
|
35
|
+
if (this.isProcessing || this.requestQueue.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.isProcessing = true;
|
|
39
|
+
const task = this.requestQueue.shift();
|
|
40
|
+
let retries = 0;
|
|
41
|
+
const execute = async () => {
|
|
42
|
+
try {
|
|
43
|
+
const result = await task.request();
|
|
44
|
+
task.resolve(result);
|
|
45
|
+
this.isProcessing = false;
|
|
46
|
+
this.processQueue();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
|
|
50
|
+
if (isAuthError && !task.authRetry) {
|
|
51
|
+
this.log.warn('API authentication failed (401). Retrying after refreshing token.');
|
|
52
|
+
this.cachedAccessToken = null;
|
|
53
|
+
this.tokenExpiry = 0;
|
|
54
|
+
task.authRetry = true;
|
|
55
|
+
setTimeout(execute, 100);
|
|
56
|
+
}
|
|
57
|
+
else if (!isAuthError && retries < MAX_RETRIES) {
|
|
58
|
+
retries++;
|
|
59
|
+
const backoffTime = INITIAL_BACKOFF_MS * Math.pow(2, retries - 1);
|
|
60
|
+
this.log.warn(`Request failed. Retrying in ${backoffTime}ms... (Attempt ${retries}/${MAX_RETRIES})`);
|
|
61
|
+
setTimeout(execute, backoffTime);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.log.error(`Request failed after ${MAX_RETRIES} retries.`, error);
|
|
65
|
+
task.reject(error);
|
|
66
|
+
this.isProcessing = false;
|
|
67
|
+
this.processQueue();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
await execute();
|
|
72
|
+
}
|
|
21
73
|
async login() {
|
|
74
|
+
return this.enqueue(() => this.performLogin());
|
|
75
|
+
}
|
|
76
|
+
async performLogin() {
|
|
22
77
|
if (!this.isTokenExpired()) {
|
|
23
78
|
return this.cachedAccessToken;
|
|
24
79
|
}
|
|
@@ -42,7 +97,6 @@ export class ShomeClient {
|
|
|
42
97
|
if (response.data && response.data.accessToken) {
|
|
43
98
|
this.cachedAccessToken = response.data.accessToken;
|
|
44
99
|
this.ihdId = response.data.ihdId;
|
|
45
|
-
// Decode token to find expiry
|
|
46
100
|
const payload = JSON.parse(Buffer.from(this.cachedAccessToken.split('.')[1], 'base64').toString());
|
|
47
101
|
this.tokenExpiry = payload.exp * 1000;
|
|
48
102
|
this.log.info('Successfully logged in to sHome API.');
|
|
@@ -55,15 +109,15 @@ export class ShomeClient {
|
|
|
55
109
|
}
|
|
56
110
|
catch (error) {
|
|
57
111
|
this.log.error(`Login error: ${error}`);
|
|
58
|
-
|
|
112
|
+
throw error;
|
|
59
113
|
}
|
|
60
114
|
}
|
|
61
115
|
async getDeviceList() {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
116
|
+
return this.enqueue(async () => {
|
|
117
|
+
const token = await this.performLogin();
|
|
118
|
+
if (!token || !this.ihdId) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
67
121
|
const createDate = this.getDateTime();
|
|
68
122
|
const hashData = this.sha512(`IHRESTAPI${this.ihdId}${createDate}`);
|
|
69
123
|
const response = await axios.get(`${BASE_URL}/v16/settings/${this.ihdId}/devices/`, {
|
|
@@ -71,18 +125,14 @@ export class ShomeClient {
|
|
|
71
125
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
72
126
|
});
|
|
73
127
|
return response.data.deviceList || [];
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
this.log.error(`Error getting device list: ${error}`);
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
128
|
+
});
|
|
79
129
|
}
|
|
80
130
|
async getDeviceInfo(thingId, type) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
131
|
+
return this.enqueue(async () => {
|
|
132
|
+
const token = await this.performLogin();
|
|
133
|
+
if (!token) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
86
136
|
const createDate = this.getDateTime();
|
|
87
137
|
const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
|
|
88
138
|
const typePath = type.toLowerCase().replace(/_/g, '');
|
|
@@ -91,18 +141,14 @@ export class ShomeClient {
|
|
|
91
141
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
92
142
|
});
|
|
93
143
|
return response.data.deviceInfoList || null;
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
this.log.error(`Error getting device info for ${thingId}: ${error}`);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
144
|
+
});
|
|
99
145
|
}
|
|
100
146
|
async setDevice(thingId, deviceId, type, controlType, state, nickname) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
147
|
+
return this.enqueue(async () => {
|
|
148
|
+
const token = await this.performLogin();
|
|
149
|
+
if (!token) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
106
152
|
const createDate = this.getDateTime();
|
|
107
153
|
const hashData = this.sha512(`IHRESTAPI${thingId}${deviceId}${state}${createDate}`);
|
|
108
154
|
const typePath = type.toLowerCase().replace(/_/g, '');
|
|
@@ -118,19 +164,14 @@ export class ShomeClient {
|
|
|
118
164
|
const displayName = nickname || `${thingId}/${deviceId}`;
|
|
119
165
|
this.log.info(`[${displayName}] state set to ${state}.`);
|
|
120
166
|
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
|
-
}
|
|
167
|
+
});
|
|
127
168
|
}
|
|
128
169
|
async unlockDoorlock(thingId, nickname) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
170
|
+
return this.enqueue(async () => {
|
|
171
|
+
const token = await this.performLogin();
|
|
172
|
+
if (!token) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
134
175
|
const createDate = this.getDateTime();
|
|
135
176
|
const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
|
|
136
177
|
await axios.put(`${BASE_URL}/v16/settings/doorlocks/${thingId}/open-mode`, null, {
|
|
@@ -144,12 +185,7 @@ export class ShomeClient {
|
|
|
144
185
|
const displayName = nickname || thingId;
|
|
145
186
|
this.log.info(`Unlocked [${displayName}].`);
|
|
146
187
|
return true;
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
const displayName = nickname || thingId;
|
|
150
|
-
this.log.error(`Error unlocking [${displayName}]: ${error}`);
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
188
|
+
});
|
|
153
189
|
}
|
|
154
190
|
sha512(input) {
|
|
155
191
|
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":"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"}
|
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,60 @@ export class ShomeClient {
|
|
|
34
46
|
) {
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
private async enqueue<T>(request: () => Promise<T>): Promise<T> {
|
|
50
|
+
return new Promise<T>((resolve, reject) => {
|
|
51
|
+
this.requestQueue.push({ request, resolve, reject, authRetry: false });
|
|
52
|
+
if (!this.isProcessing) {
|
|
53
|
+
this.processQueue();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async processQueue(): Promise<void> {
|
|
59
|
+
if (this.isProcessing || this.requestQueue.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.isProcessing = true;
|
|
63
|
+
const task = this.requestQueue.shift()!;
|
|
64
|
+
let retries = 0;
|
|
65
|
+
|
|
66
|
+
const execute = async () => {
|
|
67
|
+
try {
|
|
68
|
+
const result = await task.request();
|
|
69
|
+
task.resolve(result);
|
|
70
|
+
this.isProcessing = false;
|
|
71
|
+
this.processQueue();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
|
|
74
|
+
|
|
75
|
+
if (isAuthError && !task.authRetry) {
|
|
76
|
+
this.log.warn('API authentication failed (401). Retrying after refreshing token.');
|
|
77
|
+
this.cachedAccessToken = null;
|
|
78
|
+
this.tokenExpiry = 0;
|
|
79
|
+
task.authRetry = true;
|
|
80
|
+
setTimeout(execute, 100);
|
|
81
|
+
} else if (!isAuthError && retries < MAX_RETRIES) {
|
|
82
|
+
retries++;
|
|
83
|
+
const backoffTime = INITIAL_BACKOFF_MS * Math.pow(2, retries - 1);
|
|
84
|
+
this.log.warn(`Request failed. Retrying in ${backoffTime}ms... (Attempt ${retries}/${MAX_RETRIES})`);
|
|
85
|
+
setTimeout(execute, backoffTime);
|
|
86
|
+
} else {
|
|
87
|
+
this.log.error(`Request failed after ${MAX_RETRIES} retries.`, error);
|
|
88
|
+
task.reject(error);
|
|
89
|
+
this.isProcessing = false;
|
|
90
|
+
this.processQueue();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
await execute();
|
|
96
|
+
}
|
|
97
|
+
|
|
37
98
|
async login(): Promise<string | null> {
|
|
99
|
+
return this.enqueue(() => this.performLogin());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async performLogin(): Promise<string | null> {
|
|
38
103
|
if (!this.isTokenExpired()) {
|
|
39
104
|
return this.cachedAccessToken;
|
|
40
105
|
}
|
|
@@ -62,7 +127,6 @@ export class ShomeClient {
|
|
|
62
127
|
this.cachedAccessToken = response.data.accessToken;
|
|
63
128
|
this.ihdId = response.data.ihdId;
|
|
64
129
|
|
|
65
|
-
// Decode token to find expiry
|
|
66
130
|
const payload = JSON.parse(Buffer.from(this.cachedAccessToken!.split('.')[1], 'base64').toString());
|
|
67
131
|
this.tokenExpiry = payload.exp * 1000;
|
|
68
132
|
|
|
@@ -74,17 +138,17 @@ export class ShomeClient {
|
|
|
74
138
|
}
|
|
75
139
|
} catch (error) {
|
|
76
140
|
this.log.error(`Login error: ${error}`);
|
|
77
|
-
|
|
141
|
+
throw error;
|
|
78
142
|
}
|
|
79
143
|
}
|
|
80
144
|
|
|
81
145
|
async getDeviceList(): Promise<MainDevice[]> {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
146
|
+
return this.enqueue(async () => {
|
|
147
|
+
const token = await this.performLogin();
|
|
148
|
+
if (!token || !this.ihdId) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
86
151
|
|
|
87
|
-
try {
|
|
88
152
|
const createDate = this.getDateTime();
|
|
89
153
|
const hashData = this.sha512(`IHRESTAPI${this.ihdId}${createDate}`);
|
|
90
154
|
|
|
@@ -92,21 +156,17 @@ export class ShomeClient {
|
|
|
92
156
|
params: { createDate, hashData },
|
|
93
157
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
94
158
|
});
|
|
95
|
-
|
|
96
159
|
return response.data.deviceList || [];
|
|
97
|
-
}
|
|
98
|
-
this.log.error(`Error getting device list: ${error}`);
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
160
|
+
});
|
|
101
161
|
}
|
|
102
162
|
|
|
103
163
|
async getDeviceInfo(thingId: string, type: string): Promise<SubDevice[] | null> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
164
|
+
return this.enqueue(async () => {
|
|
165
|
+
const token = await this.performLogin();
|
|
166
|
+
if (!token) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
108
169
|
|
|
109
|
-
try {
|
|
110
170
|
const createDate = this.getDateTime();
|
|
111
171
|
const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
|
|
112
172
|
const typePath = type.toLowerCase().replace(/_/g, '');
|
|
@@ -115,21 +175,17 @@ export class ShomeClient {
|
|
|
115
175
|
params: { createDate, hashData },
|
|
116
176
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
117
177
|
});
|
|
118
|
-
|
|
119
178
|
return response.data.deviceInfoList || null;
|
|
120
|
-
}
|
|
121
|
-
this.log.error(`Error getting device info for ${thingId}: ${error}`);
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
179
|
+
});
|
|
124
180
|
}
|
|
125
181
|
|
|
126
182
|
async setDevice(thingId: string, deviceId: string, type: string, controlType: string, state: string, nickname?: string): Promise<boolean> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
183
|
+
return this.enqueue(async () => {
|
|
184
|
+
const token = await this.performLogin();
|
|
185
|
+
if (!token) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
131
188
|
|
|
132
|
-
try {
|
|
133
189
|
const createDate = this.getDateTime();
|
|
134
190
|
const hashData = this.sha512(`IHRESTAPI${thingId}${deviceId}${state}${createDate}`);
|
|
135
191
|
const typePath = type.toLowerCase().replace(/_/g, '');
|
|
@@ -147,20 +203,16 @@ export class ShomeClient {
|
|
|
147
203
|
const displayName = nickname || `${thingId}/${deviceId}`;
|
|
148
204
|
this.log.info(`[${displayName}] state set to ${state}.`);
|
|
149
205
|
return true;
|
|
150
|
-
}
|
|
151
|
-
const displayName = nickname || `${thingId}/${deviceId}`;
|
|
152
|
-
this.log.error(`Error setting device [${displayName}]: ${error}`);
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
206
|
+
});
|
|
155
207
|
}
|
|
156
208
|
|
|
157
209
|
async unlockDoorlock(thingId: string, nickname?: string): Promise<boolean> {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
210
|
+
return this.enqueue(async () => {
|
|
211
|
+
const token = await this.performLogin();
|
|
212
|
+
if (!token) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
162
215
|
|
|
163
|
-
try {
|
|
164
216
|
const createDate = this.getDateTime();
|
|
165
217
|
const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
|
|
166
218
|
|
|
@@ -176,11 +228,7 @@ export class ShomeClient {
|
|
|
176
228
|
const displayName = nickname || thingId;
|
|
177
229
|
this.log.info(`Unlocked [${displayName}].`);
|
|
178
230
|
return true;
|
|
179
|
-
}
|
|
180
|
-
const displayName = nickname || thingId;
|
|
181
|
-
this.log.error(`Error unlocking [${displayName}]: ${error}`);
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
231
|
+
});
|
|
184
232
|
}
|
|
185
233
|
|
|
186
234
|
private sha512(input: string): string {
|