@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.
@@ -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>): 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, deviceId: string, type: string, controlType: string, state: string, nickname?: string): Promise<boolean> {
223
- return this.enqueuePut(async () => {
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}${deviceId}${state}${createDate}`);
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}/${deviceId}/${controlPath}`, null, {
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 || `${thingId}/${deviceId}`;
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
- return this.enqueuePut(async () => {
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
  }