@boozilla/homebridge-shome 1.1.2 → 1.2.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/src/platform.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  import { API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service } from 'homebridge';
2
2
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
3
- import { ShomeClient, MainDevice, SubDevice, Visitor } from './shomeClient.js';
3
+ import { ShomeClient, MainDevice, SubDevice, Visitor, ParkingEvent, MaintenanceFeeData } from './shomeClient.js';
4
4
  import { LightAccessory } from './accessories/lightAccessory.js';
5
5
  import { VentilatorAccessory } from './accessories/ventilatorAccessory.js';
6
6
  import { HeaterAccessory } from './accessories/heaterAccessory.js';
7
7
  import { DoorlockAccessory } from './accessories/doorlockAccessory.js';
8
8
  import { DoorbellAccessory } from './accessories/doorbellAccessory.js';
9
+ import { ParkingAccessory } from './accessories/parkingAccessory.js';
10
+ import { MaintenanceFeeAccessory } from './accessories/maintenanceFeeAccessory.js';
9
11
 
10
12
  const CONTROLLABLE_MULTI_DEVICE_TYPES = ['LIGHT', 'HEATER', 'VENTILATOR'];
11
13
  const SPECIAL_CONTROLLABLE_TYPES = ['DOORLOCK'];
12
14
 
13
- type AccessoryHandler = LightAccessory | VentilatorAccessory | HeaterAccessory | DoorlockAccessory | DoorbellAccessory;
15
+ type AccessoryHandler = LightAccessory | VentilatorAccessory | HeaterAccessory |
16
+ DoorlockAccessory | DoorbellAccessory | ParkingAccessory | MaintenanceFeeAccessory;
14
17
 
15
18
  export class ShomePlatform implements DynamicPlatformPlugin {
16
19
  public readonly Service: typeof Service;
@@ -22,6 +25,8 @@ export class ShomePlatform implements DynamicPlatformPlugin {
22
25
  private pollingTimer?: NodeJS.Timeout;
23
26
 
24
27
  private lastCheckedTimestamp: Date = new Date();
28
+ private lastCheckedParkingTimestamp: Date = new Date();
29
+ private lastCheckedMaintenanceFeeMonth: string | null = null;
25
30
 
26
31
  constructor(
27
32
  public readonly log: Logger,
@@ -59,6 +64,12 @@ export class ShomePlatform implements DynamicPlatformPlugin {
59
64
  if (this.pollingTimer) {
60
65
  clearInterval(this.pollingTimer);
61
66
  }
67
+ // Add this loop to properly shut down handlers
68
+ for (const handler of this.accessoryHandlers.values()) {
69
+ if (handler instanceof DoorbellAccessory) {
70
+ handler.shutdown();
71
+ }
72
+ }
62
73
  });
63
74
  }
64
75
 
@@ -98,6 +109,16 @@ export class ShomePlatform implements DynamicPlatformPlugin {
98
109
  const doorbellAccessory = this.setupAccessory(doorbellDevice, null, doorbellUUID);
99
110
  foundAccessories.push(doorbellAccessory);
100
111
 
112
+ const parkingUUID = this.api.hap.uuid.generate('shome-parking');
113
+ const parkingDevice = { thngModelTypeName: 'PARKING', nickname: 'Parking Sensor', thngId: 'shome-parking' } as MainDevice;
114
+ const parkingAccessory = this.setupAccessory(parkingDevice, null, parkingUUID);
115
+ foundAccessories.push(parkingAccessory);
116
+
117
+ const feeUUID = this.api.hap.uuid.generate('shome-maintenance-fee');
118
+ const feeDevice = { thngModelTypeName: 'MAINTENANCE_FEE', nickname: 'Maintenance Fee', thngId: 'shome-maintenance-fee' } as MainDevice;
119
+ const feeAccessory = this.setupAccessory(feeDevice, null, feeUUID);
120
+ foundAccessories.push(feeAccessory);
121
+
101
122
  const accessoriesToRemove = this.accessories.filter(cachedAccessory =>
102
123
  !foundAccessories.some(foundAccessory => foundAccessory.UUID === cachedAccessory.UUID),
103
124
  );
@@ -162,6 +183,12 @@ export class ShomePlatform implements DynamicPlatformPlugin {
162
183
  case 'DOORBELL':
163
184
  this.accessoryHandlers.set(accessory.UUID, new DoorbellAccessory(this, accessory));
164
185
  break;
186
+ case 'PARKING':
187
+ this.accessoryHandlers.set(accessory.UUID, new ParkingAccessory(this, accessory));
188
+ break;
189
+ case 'MAINTENANCE_FEE':
190
+ this.accessoryHandlers.set(accessory.UUID, new MaintenanceFeeAccessory(this, accessory));
191
+ break;
165
192
  }
166
193
  }
167
194
 
@@ -172,6 +199,8 @@ export class ShomePlatform implements DynamicPlatformPlugin {
172
199
  try {
173
200
  await this.pollDeviceUpdates();
174
201
  await this.checkForNewVisitors();
202
+ await this.checkForNewParkingEvents();
203
+ await this.checkForNewMaintenanceFee();
175
204
  } catch (error) {
176
205
  this.log.error('An error occurred during polling:', error);
177
206
  }
@@ -246,6 +275,122 @@ export class ShomePlatform implements DynamicPlatformPlugin {
246
275
  }
247
276
  }
248
277
 
278
+ async checkForNewParkingEvents() {
279
+ this.log.debug('Checking for new parking events...');
280
+ const parkingEventList = await this.shomeClient.getParkingHistory();
281
+ const newParkingEvents: ParkingEvent[] = [];
282
+
283
+ for (const event of parkingEventList) {
284
+ const eventTime = new Date(event.park_date);
285
+
286
+ if (eventTime > this.lastCheckedParkingTimestamp) {
287
+ newParkingEvents.push(event);
288
+ }
289
+ }
290
+
291
+ if (newParkingEvents.length > 0) {
292
+ this.log.info(`Found ${newParkingEvents.length} new parking event(s).`);
293
+ newParkingEvents.sort((a, b) => a.park_date.localeCompare(b.park_date));
294
+
295
+ const parkingUUID = this.api.hap.uuid.generate('shome-parking');
296
+ const parkingHandler = this.accessoryHandlers.get(parkingUUID) as ParkingAccessory | undefined;
297
+
298
+ if (parkingHandler) {
299
+ for (const event of newParkingEvents) {
300
+ parkingHandler.newParkingEvent(event);
301
+ }
302
+ } else {
303
+ this.log.warn('Parking accessory handler not found.');
304
+ }
305
+
306
+ const latestEvent = newParkingEvents[newParkingEvents.length - 1];
307
+ this.lastCheckedParkingTimestamp = new Date(latestEvent.park_date);
308
+ this.log.debug(`Updated last checked parking timestamp to: ${this.lastCheckedParkingTimestamp.toISOString()}`);
309
+ }
310
+ }
311
+
312
+ async checkForNewMaintenanceFee() {
313
+ this.log.debug('Checking for new maintenance fee...');
314
+
315
+ if (!this.lastCheckedMaintenanceFeeMonth) {
316
+ this.log.debug('First run for maintenance fee check. Finding the latest available data...');
317
+ const now = new Date();
318
+ let initialFeeData: MaintenanceFeeData | null = null;
319
+ let initialYear = now.getFullYear();
320
+ let initialMonth = now.getMonth() + 1;
321
+
322
+ for (let i = 0; i < 3; i++) {
323
+ const feeData = await this.shomeClient.getMaintenanceFee(initialYear, initialMonth);
324
+ if (feeData && feeData.expense_total && feeData.expense_total.length > 0 && feeData.expense_total[0].money > 0) {
325
+ initialFeeData = feeData;
326
+ break;
327
+ }
328
+
329
+ initialMonth--;
330
+ if (initialMonth === 0) {
331
+ initialMonth = 12;
332
+ initialYear--;
333
+ }
334
+ }
335
+
336
+ if (initialFeeData) {
337
+ const monthStr = `${initialFeeData.search_year}-${initialFeeData.search_month}`;
338
+ this.log.info(`Found initial latest maintenance fee data for ${monthStr}.`);
339
+ this.lastCheckedMaintenanceFeeMonth = monthStr;
340
+ } else {
341
+ this.log.debug('Could not find any maintenance fee data for the last 3 months on first run.');
342
+
343
+ const twoMonthsAgo = new Date();
344
+ twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
345
+ this.lastCheckedMaintenanceFeeMonth = `${twoMonthsAgo.getFullYear()}-${String(twoMonthsAgo.getMonth() + 1).padStart(2, '0')}`;
346
+ this.log.debug(`Setting baseline check month to ${this.lastCheckedMaintenanceFeeMonth}.`);
347
+ }
348
+ return;
349
+ }
350
+
351
+ let [lastYear, lastMonth] = this.lastCheckedMaintenanceFeeMonth.split('-').map(Number);
352
+
353
+ for (let i = 0; i < 3; i++) {
354
+ let nextMonth = lastMonth + 1;
355
+ let nextYear = lastYear;
356
+ if (nextMonth > 12) {
357
+ nextMonth = 1;
358
+ nextYear++;
359
+ }
360
+
361
+ const now = new Date();
362
+
363
+ if (nextYear > now.getFullYear() || (nextYear === now.getFullYear() && nextMonth > now.getMonth() + 1)) {
364
+ break;
365
+ }
366
+
367
+ const nextMonthStr = `${nextYear}-${String(nextMonth).padStart(2, '0')}`;
368
+ this.log.debug(`Proactively checking for maintenance fee for ${nextMonthStr}...`);
369
+ const feeData = await this.shomeClient.getMaintenanceFee(nextYear, nextMonth);
370
+
371
+ if (feeData && feeData.expense_total && feeData.expense_total.length > 0 && feeData.expense_total[0].money > 0) {
372
+ this.log.info(`Found new maintenance fee data for ${nextMonthStr}.`);
373
+
374
+ const feeUUID = this.api.hap.uuid.generate('shome-maintenance-fee');
375
+ const feeHandler = this.accessoryHandlers.get(feeUUID) as MaintenanceFeeAccessory | undefined;
376
+ if (feeHandler) {
377
+ feeHandler.triggerNotification(feeData);
378
+ this.lastCheckedMaintenanceFeeMonth = nextMonthStr;
379
+ this.log.debug(`Last checked maintenance fee month updated to: ${nextMonthStr}`);
380
+
381
+ lastYear = nextYear;
382
+ lastMonth = nextMonth;
383
+ } else {
384
+ this.log.warn('Maintenance fee accessory handler not found.');
385
+ break;
386
+ }
387
+ } else {
388
+ this.log.debug(`No maintenance fee data found for ${nextMonthStr}. Stopping check for this cycle.`);
389
+ break;
390
+ }
391
+ }
392
+ }
393
+
249
394
  private parseRecordDt(recordDt: string): Date {
250
395
  const year = parseInt(recordDt.substring(0, 4), 10);
251
396
  const month = parseInt(recordDt.substring(4, 6), 10) - 1;
@@ -31,6 +31,35 @@ export interface Visitor {
31
31
  deviceLabel: string;
32
32
  }
33
33
 
34
+ export interface ParkingEvent {
35
+ car_no: string;
36
+ park_date: string;
37
+ unit: 'in' | 'out';
38
+ }
39
+
40
+ export interface ExpenseItem {
41
+ money: number;
42
+ name: string;
43
+ }
44
+
45
+ export interface ExpenseBundle {
46
+ money: number;
47
+ name: string;
48
+ }
49
+
50
+ export interface ExpenseTotal {
51
+ money: number;
52
+ name: string;
53
+ }
54
+
55
+ export interface MaintenanceFeeData {
56
+ search_year: string;
57
+ search_month: string;
58
+ expense_item: ExpenseItem[];
59
+ expense_bundle: ExpenseBundle[];
60
+ expense_total: ExpenseTotal[];
61
+ }
62
+
34
63
  type QueueTask<T = unknown> = {
35
64
  request: () => Promise<T>;
36
65
  resolve: (value: T | PromiseLike<T>) => void;
@@ -315,6 +344,47 @@ export class ShomeClient {
315
344
  });
316
345
  }
317
346
 
347
+ async getParkingHistory(): Promise<ParkingEvent[]> {
348
+ return this.executeWithRetries(async () => {
349
+ const token = this.cachedAccessToken;
350
+ if (!token || !this.homeId) {
351
+ this.log.error('Cannot fetch parking history: Not logged in or homeId is missing.');
352
+ return [];
353
+ }
354
+
355
+ const createDate = this.getDateTime();
356
+ const hashData = this.sha512(`IHRESTAPI${this.homeId}${createDate}`);
357
+ const response = await axios.get(`${BASE_URL}/v18/complex/${this.homeId}/parking/inout-histories`, {
358
+ params: { createDate, hashData },
359
+ headers: { 'Authorization': `Bearer ${token}` },
360
+ });
361
+
362
+ return response.data.data || [];
363
+ });
364
+ }
365
+
366
+ async getMaintenanceFee(year: number, month: number): Promise<MaintenanceFeeData | null> {
367
+ return this.executeWithRetries(async () => {
368
+ const token = this.cachedAccessToken;
369
+ if (!token || !this.homeId) {
370
+ this.log.error('Cannot fetch maintenance fee: Not logged in or homeId is missing.');
371
+ return null;
372
+ }
373
+
374
+ const createDate = this.getDateTime();
375
+ const hashData = this.sha512(`IHRESTAPI${this.homeId}${year}${month}${createDate}`);
376
+ const response = await axios.get(`${BASE_URL}/v18/complex/${this.homeId}/maintenance-fee/${year}/${month}`, {
377
+ params: { createDate, hashData },
378
+ headers: { 'Authorization': `Bearer ${token}` },
379
+ });
380
+
381
+ if (response.data && response.data.data && response.data.data.length > 0) {
382
+ return response.data.data[0];
383
+ }
384
+ return null;
385
+ });
386
+ }
387
+
318
388
  async getThumbnailImage(visitor: Visitor): Promise<Buffer | null> {
319
389
  const request = async () => {
320
390
  const token = this.cachedAccessToken;
@@ -346,7 +416,6 @@ export class ShomeClient {
346
416
  return this.executeWithRetries(request);
347
417
  }
348
418
 
349
-
350
419
  private sha512(input: string): string {
351
420
  return CryptoJS.SHA512(input).toString();
352
421
  }