@boozilla/homebridge-shome 1.0.12 → 1.1.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.
- package/README.md +4 -1
- package/dist/accessories/doorbellAccessory.d.ts +12 -0
- package/dist/accessories/doorbellAccessory.js +38 -0
- package/dist/accessories/doorbellAccessory.js.map +1 -0
- package/dist/controller/cameraController.d.ts +16 -0
- package/dist/controller/cameraController.js +71 -0
- package/dist/controller/cameraController.js.map +1 -0
- package/dist/platform.d.ts +5 -1
- package/dist/platform.js +100 -43
- package/dist/platform.js.map +1 -1
- package/dist/shomeClient.d.ts +12 -1
- package/dist/shomeClient.js +70 -12
- package/dist/shomeClient.js.map +1 -1
- package/package.json +1 -1
- package/src/accessories/doorbellAccessory.ts +49 -0
- package/src/controller/cameraController.ts +92 -0
- package/src/platform.ts +115 -45
- package/src/shomeClient.ts +88 -12
package/src/shomeClient.ts
CHANGED
|
@@ -24,20 +24,30 @@ export interface SubDevice {
|
|
|
24
24
|
[key: string]: unknown;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export interface Visitor {
|
|
28
|
+
sttId: string;
|
|
29
|
+
thumbNail: string;
|
|
30
|
+
recodDt: string;
|
|
31
|
+
deviceLabel: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
type QueueTask<T = unknown> = {
|
|
28
35
|
request: () => Promise<T>;
|
|
29
36
|
resolve: (value: T | PromiseLike<T>) => void;
|
|
30
37
|
reject: (reason?: unknown) => void;
|
|
38
|
+
deviceId?: string;
|
|
31
39
|
};
|
|
32
40
|
|
|
33
41
|
export class ShomeClient {
|
|
34
42
|
private cachedAccessToken: string | null = null;
|
|
35
43
|
private ihdId: string | null = null;
|
|
44
|
+
private homeId: string | null = null;
|
|
36
45
|
private tokenExpiry: number = 0;
|
|
37
46
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
47
|
private putQueue: QueueTask<any>[] = [];
|
|
39
48
|
private isProcessingPut = false;
|
|
40
49
|
private loginPromise: Promise<string | null> | null = null;
|
|
50
|
+
private pendingPutRequests = new Set<string>();
|
|
41
51
|
|
|
42
52
|
constructor(
|
|
43
53
|
private readonly log: Logger,
|
|
@@ -100,6 +110,7 @@ export class ShomeClient {
|
|
|
100
110
|
if (response.data && response.data.accessToken) {
|
|
101
111
|
this.cachedAccessToken = response.data.accessToken;
|
|
102
112
|
this.ihdId = response.data.ihdId;
|
|
113
|
+
this.homeId = response.data.homeId;
|
|
103
114
|
|
|
104
115
|
const payload = JSON.parse(Buffer.from(this.cachedAccessToken!.split('.')[1], 'base64').toString());
|
|
105
116
|
this.tokenExpiry = payload.exp * 1000;
|
|
@@ -116,9 +127,9 @@ export class ShomeClient {
|
|
|
116
127
|
}
|
|
117
128
|
}
|
|
118
129
|
|
|
119
|
-
private enqueuePut<T>(request: () => Promise<T
|
|
130
|
+
private enqueuePut<T>(request: () => Promise<T>, deviceId?: string): Promise<T> {
|
|
120
131
|
return new Promise<T>((resolve, reject) => {
|
|
121
|
-
this.putQueue.push({ request, resolve, reject });
|
|
132
|
+
this.putQueue.push({ request, resolve, reject, deviceId });
|
|
122
133
|
this.processPutQueue();
|
|
123
134
|
});
|
|
124
135
|
}
|
|
@@ -131,11 +142,18 @@ export class ShomeClient {
|
|
|
131
142
|
|
|
132
143
|
while (this.putQueue.length > 0) {
|
|
133
144
|
const task = this.putQueue.shift()!;
|
|
145
|
+
if (task.deviceId) {
|
|
146
|
+
this.pendingPutRequests.add(task.deviceId);
|
|
147
|
+
}
|
|
134
148
|
try {
|
|
135
149
|
const result = await this.executeWithRetries(task.request, true);
|
|
136
150
|
task.resolve(result);
|
|
137
151
|
} catch (error) {
|
|
138
152
|
task.reject(error);
|
|
153
|
+
} finally {
|
|
154
|
+
if (task.deviceId) {
|
|
155
|
+
this.pendingPutRequests.delete(task.deviceId);
|
|
156
|
+
}
|
|
139
157
|
}
|
|
140
158
|
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
141
159
|
}
|
|
@@ -147,8 +165,6 @@ export class ShomeClient {
|
|
|
147
165
|
let retries = 0;
|
|
148
166
|
while (true) {
|
|
149
167
|
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
168
|
if (!isQueued) {
|
|
153
169
|
await this.login();
|
|
154
170
|
}
|
|
@@ -174,12 +190,15 @@ export class ShomeClient {
|
|
|
174
190
|
}
|
|
175
191
|
await new Promise(resolve => setTimeout(resolve, backoffTime));
|
|
176
192
|
|
|
177
|
-
// After a failure, always ensure we are logged in before the next attempt.
|
|
178
193
|
await this.login();
|
|
179
194
|
}
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
197
|
|
|
198
|
+
public isDeviceBusy(deviceId: string): boolean {
|
|
199
|
+
return this.pendingPutRequests.has(deviceId);
|
|
200
|
+
}
|
|
201
|
+
|
|
183
202
|
async getDeviceList(): Promise<MainDevice[]> {
|
|
184
203
|
return this.executeWithRetries(async () => {
|
|
185
204
|
const token = this.cachedAccessToken;
|
|
@@ -219,19 +238,20 @@ export class ShomeClient {
|
|
|
219
238
|
});
|
|
220
239
|
}
|
|
221
240
|
|
|
222
|
-
async setDevice(thingId: string,
|
|
223
|
-
|
|
241
|
+
async setDevice(thingId: string, subDeviceId: string, type: string, controlType: string, state: string, nickname?: string): Promise<boolean> {
|
|
242
|
+
const deviceId = `${thingId}-${subDeviceId}`;
|
|
243
|
+
const request = async () => {
|
|
224
244
|
const token = this.cachedAccessToken;
|
|
225
245
|
if (!token) {
|
|
226
246
|
return false;
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
const createDate = this.getDateTime();
|
|
230
|
-
const hashData = this.sha512(`IHRESTAPI${thingId}${
|
|
250
|
+
const hashData = this.sha512(`IHRESTAPI${thingId}${subDeviceId}${state}${createDate}`);
|
|
231
251
|
const typePath = type.toLowerCase().replace(/_/g, '');
|
|
232
252
|
const controlPath = controlType.toLowerCase().replace(/_/g, '-');
|
|
233
253
|
|
|
234
|
-
await axios.put(`${BASE_URL}/v18/settings/${typePath}/${thingId}/${
|
|
254
|
+
await axios.put(`${BASE_URL}/v18/settings/${typePath}/${thingId}/${subDeviceId}/${controlPath}`, null, {
|
|
235
255
|
params: {
|
|
236
256
|
createDate,
|
|
237
257
|
[controlType === 'WINDSPEED' ? 'mode' : 'state']: state,
|
|
@@ -240,14 +260,17 @@ export class ShomeClient {
|
|
|
240
260
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
241
261
|
});
|
|
242
262
|
|
|
243
|
-
const displayName = nickname ||
|
|
263
|
+
const displayName = nickname || deviceId;
|
|
244
264
|
this.log.info(`[${displayName}] state set to ${state}.`);
|
|
245
265
|
return true;
|
|
246
|
-
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return this.enqueuePut(request, deviceId);
|
|
247
269
|
}
|
|
248
270
|
|
|
249
271
|
async unlockDoorlock(thingId: string, nickname?: string): Promise<boolean> {
|
|
250
|
-
|
|
272
|
+
const deviceId = thingId;
|
|
273
|
+
const request = async () => {
|
|
251
274
|
const token = this.cachedAccessToken;
|
|
252
275
|
if (!token) {
|
|
253
276
|
return false;
|
|
@@ -268,9 +291,62 @@ export class ShomeClient {
|
|
|
268
291
|
const displayName = nickname || thingId;
|
|
269
292
|
this.log.info(`Unlocked [${displayName}].`);
|
|
270
293
|
return true;
|
|
294
|
+
};
|
|
295
|
+
return this.enqueuePut(request, deviceId);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async getVisitorHistory(): Promise<Visitor[]> {
|
|
299
|
+
return this.executeWithRetries(async () => {
|
|
300
|
+
const token = this.cachedAccessToken;
|
|
301
|
+
if (!token || !this.homeId) {
|
|
302
|
+
this.log.error('Cannot fetch visitor history: Not logged in or homeId is missing.');
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const createDate = this.getDateTime();
|
|
307
|
+
const offset = 0;
|
|
308
|
+
const hashData = this.sha512(`IHRESTAPI${this.homeId}${offset}${createDate}`);
|
|
309
|
+
const response = await axios.get(`${BASE_URL}/v16/histories/${this.homeId}/video-histories`, {
|
|
310
|
+
params: { createDate, hashData, offset },
|
|
311
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return response.data.videoList || [];
|
|
271
315
|
});
|
|
272
316
|
}
|
|
273
317
|
|
|
318
|
+
async getThumbnailImage(visitor: Visitor): Promise<Buffer | null> {
|
|
319
|
+
const request = async () => {
|
|
320
|
+
const token = this.cachedAccessToken;
|
|
321
|
+
if (!token) {
|
|
322
|
+
this.log.error('Cannot fetch thumbnail: Not logged in.');
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!visitor.sttId) {
|
|
327
|
+
this.log.error('Cannot fetch thumbnail: sttId is missing from visitor object.');
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const createDate = this.getDateTime();
|
|
332
|
+
const hashData = this.sha512(`IHRESTAPI${visitor.sttId}${createDate}`);
|
|
333
|
+
const thumbnailUrl = `${BASE_URL}/v16/histories/${visitor.sttId}/video-thumbnail`;
|
|
334
|
+
const response = await axios.get(thumbnailUrl, {
|
|
335
|
+
params: {
|
|
336
|
+
createDate,
|
|
337
|
+
hashData,
|
|
338
|
+
},
|
|
339
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
340
|
+
responseType: 'arraybuffer',
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return Buffer.from(response.data, 'binary');
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return this.executeWithRetries(request);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
274
350
|
private sha512(input: string): string {
|
|
275
351
|
return CryptoJS.SHA512(input).toString();
|
|
276
352
|
}
|