@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/dist/accessories/doorbellAccessory.d.ts +1 -0
- package/dist/accessories/doorbellAccessory.js +3 -0
- package/dist/accessories/doorbellAccessory.js.map +1 -1
- package/dist/accessories/maintenanceFeeAccessory.d.ts +10 -0
- package/dist/accessories/maintenanceFeeAccessory.js +27 -0
- package/dist/accessories/maintenanceFeeAccessory.js.map +1 -0
- package/dist/accessories/parkingAccessory.d.ts +10 -0
- package/dist/accessories/parkingAccessory.js +23 -0
- package/dist/accessories/parkingAccessory.js.map +1 -0
- package/dist/controller/cameraController.d.ts +7 -3
- package/dist/controller/cameraController.js +170 -14
- package/dist/controller/cameraController.js.map +1 -1
- package/dist/platform.d.ts +4 -0
- package/dist/platform.js +125 -0
- package/dist/platform.js.map +1 -1
- package/dist/shomeClient.d.ts +26 -0
- package/dist/shomeClient.js +35 -0
- package/dist/shomeClient.js.map +1 -1
- package/package.json +3 -2
- package/src/accessories/doorbellAccessory.ts +4 -0
- package/src/accessories/maintenanceFeeAccessory.ts +34 -0
- package/src/accessories/parkingAccessory.ts +29 -0
- package/src/controller/cameraController.ts +222 -15
- package/src/platform.ts +147 -2
- package/src/shomeClient.ts +70 -1
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 |
|
|
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;
|
package/src/shomeClient.ts
CHANGED
|
@@ -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
|
}
|