homebridge-ttlock-accesscode 1.0.1 → 2.0.0-beta.1

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.
Files changed (53) hide show
  1. package/README.md +5 -5
  2. package/config.schema.json +31 -4
  3. package/dist/api/ttlockApi.d.ts +9 -8
  4. package/dist/api/ttlockApi.js +128 -67
  5. package/dist/api/ttlockApi.js.map +1 -1
  6. package/dist/api/usageTracker.d.ts +37 -0
  7. package/dist/api/usageTracker.js +276 -0
  8. package/dist/api/usageTracker.js.map +1 -0
  9. package/dist/config.d.ts +6 -0
  10. package/dist/config.js +23 -11
  11. package/dist/config.js.map +1 -1
  12. package/dist/devices/accessoryInformation.d.ts +3 -0
  13. package/dist/devices/accessoryInformation.js +28 -0
  14. package/dist/devices/accessoryInformation.js.map +1 -0
  15. package/dist/devices/baseDevice.d.ts +58 -0
  16. package/dist/devices/baseDevice.js +376 -0
  17. package/dist/devices/baseDevice.js.map +1 -0
  18. package/dist/devices/create.d.ts +4 -0
  19. package/dist/devices/create.js +27 -0
  20. package/dist/devices/create.js.map +1 -0
  21. package/dist/devices/descriptorHelpers.d.ts +5 -0
  22. package/dist/devices/descriptorHelpers.js +82 -0
  23. package/dist/devices/descriptorHelpers.js.map +1 -0
  24. package/dist/devices/deviceManager.d.ts +19 -0
  25. package/dist/devices/deviceManager.js +74 -0
  26. package/dist/devices/deviceManager.js.map +1 -0
  27. package/dist/devices/deviceTypes.d.ts +44 -0
  28. package/dist/devices/deviceTypes.js +2 -0
  29. package/dist/devices/deviceTypes.js.map +1 -0
  30. package/dist/devices/homekitLock.d.ts +18 -0
  31. package/dist/devices/homekitLock.js +191 -0
  32. package/dist/devices/homekitLock.js.map +1 -0
  33. package/dist/index.js +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/platform.d.ts +45 -19
  36. package/dist/platform.js +397 -234
  37. package/dist/platform.js.map +1 -1
  38. package/dist/taskQueue.d.ts +12 -0
  39. package/dist/taskQueue.js +50 -0
  40. package/dist/taskQueue.js.map +1 -0
  41. package/dist/utils.d.ts +10 -2
  42. package/dist/utils.js +50 -5
  43. package/dist/utils.js.map +1 -1
  44. package/package.json +32 -21
  45. package/dist/homekitDevice.d.ts +0 -44
  46. package/dist/homekitDevice.js +0 -502
  47. package/dist/homekitDevice.js.map +0 -1
  48. package/dist/platformAccessoryInformation.d.ts +0 -3
  49. package/dist/platformAccessoryInformation.js +0 -35
  50. package/dist/platformAccessoryInformation.js.map +0 -1
  51. package/dist/types/index.d.ts +0 -39
  52. package/dist/types/index.js +0 -2
  53. package/dist/types/index.js.map +0 -1
package/dist/platform.js CHANGED
@@ -1,336 +1,499 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import create from './devices/create.js';
3
+ import DeviceManager from './devices/deviceManager.js';
2
4
  import { parseConfig } from './config.js';
3
- import { TTLockHomeKitDevice } from './homekitDevice.js';
4
5
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
5
- import { lookup, lookupCharacteristicNameByUUID, isObjectLike } from './utils.js';
6
+ import { TaskQueue } from './taskQueue.js';
7
+ import { deferAndCombine, isObjectLike, loadPackageConfig, lookup, lookupCharacteristicNameByUUID, satisfiesVersion, } from './utils.js';
6
8
  import { TTLockApi } from './api/ttlockApi.js';
7
- export class TTLockAccessCodePlatform {
9
+ import { UsageTracker } from './api/usageTracker.js';
10
+ import { deviceEventEmitter } from './devices/deviceManager.js';
11
+ let packageConfig;
12
+ export default class TTLockAccessCodePlatform {
8
13
  log;
9
14
  api;
10
- homekitDevicesById = new Map();
15
+ Characteristic;
11
16
  configuredAccessories = new Map();
12
17
  offlineAccessories = new Map();
18
+ Service;
13
19
  config;
20
+ deviceManager;
14
21
  isShuttingDown = false;
15
- ongoingTasks = [];
16
- periodicDeviceDiscoveryEmitter = new EventEmitter();
17
22
  periodicDeviceDiscovering = false;
23
+ periodicDeviceDiscoveryEmitter;
18
24
  ttLockApi;
25
+ usageTracker;
26
+ taskQueue;
27
+ homekitDevicesById = new Map();
28
+ deviceDiscoveredHandler;
29
+ platformInitialization;
19
30
  constructor(log, config, api) {
20
31
  this.log = log;
21
32
  this.api = api;
33
+ this.Service = this.api.hap.Service;
34
+ this.Characteristic = this.api.hap.Characteristic;
22
35
  this.config = parseConfig(config);
36
+ this.periodicDeviceDiscoveryEmitter = new EventEmitter();
37
+ this.taskQueue = new TaskQueue(this.log, () => this.isShuttingDown);
38
+ this.periodicDeviceDiscoveryEmitter.setMaxListeners(255);
39
+ this.setupDeviceEventEmitter('firstDiscovery');
40
+ this.platformInitialization = this.initializePlatform();
23
41
  this.api.on('didFinishLaunching', async () => {
24
- this.log.info('TTLockAccessCode Platform finished launching');
42
+ this.log.debug('TTLockAccessCode Platform finished launching');
43
+ await this.platformInitialization;
44
+ await this.didFinishLaunching();
45
+ if (this.offlineAccessories.size > 0) {
46
+ this.log.debug('Unregistering offline accessories');
47
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, Array.from(this.offlineAccessories.values()));
48
+ this.offlineAccessories.clear();
49
+ }
50
+ });
51
+ this.api.on('shutdown', async () => {
52
+ this.log.debug('TTLockAccessCode Platform shutting down');
53
+ if (!this.isShuttingDown) {
54
+ this.isShuttingDown = true;
55
+ }
56
+ this.log.debug('Stopping all polling tasks');
57
+ for (const device of this.homekitDevicesById.values()) {
58
+ await device.stopPolling();
59
+ }
60
+ this.log.debug('Waiting for tasks to complete');
25
61
  try {
26
- await this.createAndAuthenticateApi();
27
- await this.discoverDevices();
28
- this.log.debug('Setting up periodic device discovery');
29
- setInterval(async () => {
30
- await this.periodicDeviceDiscovery();
31
- }, this.config.discoveryOptions.discoveryPollingInterval);
32
- this.log.debug('Periodic device discovery setup completed');
33
- if (this.offlineAccessories.size > 0) {
34
- this.log.debug('Unregistering offline accessories');
35
- this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, Array.from(this.offlineAccessories.values()));
36
- this.offlineAccessories.clear();
37
- }
62
+ await this.taskQueue.waitForEmptyQueue();
38
63
  }
39
64
  catch (error) {
40
- this.log.error('An error occurred during startup:', error);
65
+ this.log.error('Error while waiting for task queue to empty during shutdown:', error);
41
66
  }
67
+ this.stopTTLockApi();
42
68
  });
43
- this.api.on('shutdown', async () => {
44
- this.log.debug('TTLockAccessCode platform shutting down');
45
- this.isShuttingDown = true;
46
- await Promise.all(this.ongoingTasks);
47
- this.log.debug('All ongoing tasks completed. Platform is now shutting down.');
48
- });
49
- this.periodicDeviceDiscoveryEmitter.setMaxListeners(150);
50
69
  }
51
- async createAndAuthenticateApi() {
52
- this.log.debug('Creating and authenticating TTLock API');
53
- try {
54
- this.ttLockApi = new TTLockApi(this.log, this.config.clientId, this.config.clientSecret);
55
- await this.ttLockApi.authenticate(this.config.username, this.config.password);
70
+ setupDeviceEventEmitter(mode, discoveredDeviceIds) {
71
+ if (this.deviceDiscoveredHandler) {
72
+ deviceEventEmitter.off('deviceDiscovered', this.deviceDiscoveredHandler);
56
73
  }
57
- catch (error) {
58
- this.log.error('Failed to authenticate with TTLock API:', error);
74
+ this.log.debug(`Setting up device event emitter: ${mode}`);
75
+ if (mode === 'periodicDiscovery' && discoveredDeviceIds) {
76
+ this.deviceDiscoveredHandler = async (device) => {
77
+ this.log.debug(`Device discovered during periodic discovery: ${device.sys_info.device_id}`);
78
+ discoveredDeviceIds.add(device.sys_info.device_id);
79
+ this.log.debug(`Added device ID to discoveredDeviceIds: ${device.sys_info.device_id}`);
80
+ await this.processDevice(device);
81
+ };
59
82
  }
60
- }
61
- async discoverDevices() {
62
- this.log.debug('Discovering devices');
63
- if (!this.ttLockApi) {
64
- this.log.error('TTLock API not initialized');
65
- return;
83
+ else {
84
+ this.deviceDiscoveredHandler = async (device) => {
85
+ this.log.debug(`Device discovered during initial discovery: ${device.sys_info.device_id}`);
86
+ await this.processDevice(device);
87
+ };
66
88
  }
89
+ deviceEventEmitter.on('deviceDiscovered', this.deviceDiscoveredHandler);
90
+ }
91
+ async initializePlatform() {
92
+ packageConfig = await loadPackageConfig(this.log);
93
+ this.logInitializationDetails();
94
+ await this.verifyEnvironment();
95
+ }
96
+ logInitializationDetails() {
97
+ this.log.info(`${packageConfig.name} v${packageConfig.version}, node ${process.version}, ` +
98
+ `homebridge v${this.api.serverVersion}, api v${this.api.version} Initializing...`);
99
+ }
100
+ async verifyEnvironment() {
101
+ this.log.debug('Verifying environment');
67
102
  try {
68
- const locks = await this.ttLockApi.getLocks();
69
- const detailedLocks = await this.getDetailedLocks(locks);
70
- for (const detailedLock of detailedLocks) {
71
- this.foundDevice(detailedLock);
103
+ this.log.debug('Checking Node.js version');
104
+ if (!satisfiesVersion(process.version, packageConfig.engines.node)) {
105
+ this.log.error(`Error: not using minimum node version ${packageConfig.engines.node}`);
106
+ }
107
+ else {
108
+ this.log.debug(`Node.js version ${process.version} satisfies the requirement ${packageConfig.engines.node}`);
109
+ }
110
+ this.log.debug('Checking Homebridge version');
111
+ if (this.api.versionGreaterOrEqual &&
112
+ !(this.api.versionGreaterOrEqual('1.11.0') ||
113
+ this.api.versionGreaterOrEqual('2.0.0'))) {
114
+ throw new Error(`homebridge-ttlock-accesscode requires Homebridge ^1.11.0 || ^2.0.0-beta.0. Currently running: ${this.api.serverVersion}`);
115
+ }
116
+ else {
117
+ this.log.debug(`Homebridge version ${this.api.serverVersion} satisfies the requirement ^1.11.0 || ^2.0.0-beta.0`);
72
118
  }
73
- this.periodicDeviceDiscoveryEmitter.setMaxListeners(detailedLocks.length + 10);
74
- const maxListenerCount = this.periodicDeviceDiscoveryEmitter.getMaxListeners();
75
- this.log.debug('periodicDeviceDiscoveryEmitter max listener count:', maxListenerCount);
76
119
  }
77
120
  catch (error) {
78
- this.log.error('Error discovering devices:', error);
121
+ this.log.error('Error verifying environment:', error);
122
+ throw error;
79
123
  }
80
124
  }
81
- async getDetailedLocks(locks) {
82
- if (!this.ttLockApi) {
83
- return Promise.reject(new Error('TTLock API not initialized'));
84
- }
85
- const detailedLocks = [];
86
- for (const lock of locks) {
87
- this.log.debug(`Processing lock: ${lock.lockId}`);
88
- const lockDetails = await this.ttLockApi.getLockDetails(lock.lockId);
89
- const lockState = await this.ttLockApi.getLockState(lock.lockId);
90
- const lockBattery = await this.ttLockApi.getBatteryLevel(lock.lockId);
91
- const lockPassCodes = await this.ttLockApi.getPasscodes(lock.lockId);
92
- const detailedLock = await this.ttLockApi.mapToLock(lockDetails, lockState, lockBattery, lockPassCodes);
93
- this.log.debug(`Detailed lock: ${JSON.stringify(detailedLock)}`);
94
- detailedLocks.push(detailedLock);
95
- }
96
- return detailedLocks;
97
- }
98
- foundDevice(device) {
99
- const { alias: deviceAlias, id: deviceId } = device;
100
- if (!deviceId) {
101
- this.log.error('Missing deviceId:', deviceAlias);
102
- return;
125
+ async didFinishLaunching() {
126
+ this.log.debug('Finished launching');
127
+ try {
128
+ await this.startTTLockApi();
129
+ this.log.debug('Initializing DeviceManager');
130
+ this.deviceManager = new DeviceManager(this);
131
+ this.log.debug('DeviceManager initialized');
132
+ await this.discoverDevices();
133
+ this.log.debug('Device discovery completed');
134
+ const discoveredDeviceIds = new Set();
135
+ this.setupPeriodicDiscovery(discoveredDeviceIds);
103
136
  }
104
- if (this.homekitDevicesById.has(deviceId)) {
105
- this.log.info(`HomeKit device already added: [${deviceAlias}] [${deviceId}]`);
106
- return;
137
+ catch (error) {
138
+ this.log.error('An error occurred during startup:', error);
107
139
  }
108
- this.log.info(`Adding HomeKit device: [${deviceAlias}] [${deviceId}]`);
109
- const homekitDevice = new TTLockHomeKitDevice(this, device);
110
- if (homekitDevice) {
111
- this.homekitDevicesById.set(deviceId, homekitDevice);
112
- this.log.debug(`HomeKit device [${deviceAlias}] [${deviceId}] successfully added`);
140
+ }
141
+ setupPeriodicDiscovery(discoveredDeviceIds) {
142
+ this.log.debug('Setting up periodic device discovery');
143
+ this.setupDeviceEventEmitter('periodicDiscovery', discoveredDeviceIds);
144
+ const discoveryTask = async () => {
145
+ await this.periodicDeviceDiscovery(discoveredDeviceIds);
146
+ };
147
+ const deferredDiscoveryTask = deferAndCombine(discoveryTask, this.config.advancedOptions.waitTimeUpdate);
148
+ setInterval(() => {
149
+ try {
150
+ this.taskQueue.addTask(deferredDiscoveryTask);
151
+ }
152
+ catch (error) {
153
+ this.log.error('Error scheduling periodic device discovery:', error);
154
+ }
155
+ }, this.config.discoveryOptions.discoveryPollingInterval);
156
+ this.log.debug('Periodic device discovery setup completed');
157
+ }
158
+ async discoverDevices() {
159
+ try {
160
+ if (this.deviceManager) {
161
+ try {
162
+ const deviceCount = this.configuredAccessories.size || 0;
163
+ const callsForDiscovery = 1 + (2 * deviceCount);
164
+ if (this.usageTracker) {
165
+ const reserved = await this.usageTracker.beginBatch(callsForDiscovery, 'initialDiscovery');
166
+ if (!reserved) {
167
+ this.log.info('Skipping initial device discovery due to API usage budget');
168
+ return;
169
+ }
170
+ }
171
+ }
172
+ catch (error) {
173
+ this.log.debug('Error reserving budget for initial discovery', error);
174
+ }
175
+ await this.deviceManager.discoverDevices();
176
+ }
113
177
  }
114
- else {
115
- this.log.error(`Failed to add HomeKit device for: [${deviceAlias}] [${deviceId}]`);
178
+ catch (error) {
179
+ this.log.error('Error during discoverDevices:', error);
116
180
  }
117
181
  }
118
- async periodicDeviceDiscovery() {
182
+ async periodicDeviceDiscovery(discoveredDeviceIds) {
183
+ this.log.debug('Starting periodic device discovery');
119
184
  if (this.periodicDeviceDiscovering) {
120
- this.log.debug('Periodic device discovery is already in progress');
185
+ this.log.debug('Periodic device discovery already in progress');
121
186
  return;
122
187
  }
123
- if (!this.ttLockApi) {
124
- this.log.error('TTLock API not initialized');
188
+ if (this.isShuttingDown) {
189
+ this.log.debug('Platform is shutting down, skipping periodic device discovery');
125
190
  return;
126
191
  }
127
192
  this.periodicDeviceDiscovering = true;
193
+ discoveredDeviceIds.clear();
194
+ this.log.debug('Cleared discoveredDeviceIds set before discovery.');
128
195
  try {
129
- this.log.debug('Starting periodic device discovery');
130
- const locks = await this.ttLockApi.getLocks();
131
- const detailedLocks = await this.getDetailedLocks(locks);
132
- const now = new Date();
133
- const offlineInterval = this.config.discoveryOptions.offlineInterval;
134
- this.configuredAccessories.forEach((platformAccessory, uuid) => {
135
- const deviceId = platformAccessory.context.id;
136
- if (deviceId) {
137
- const device = this.findDiscoveredDevice(detailedLocks, platformAccessory);
138
- if (device) {
139
- this.updateAccessoryDeviceStatus(platformAccessory, device, now);
140
- this.updateOrCreateHomeKitDevice(deviceId, device);
141
- }
142
- else {
143
- this.updateAccessoryStatus(platformAccessory);
144
- this.handleOfflineAccessory(platformAccessory, uuid, now, offlineInterval);
196
+ if (this.deviceManager) {
197
+ try {
198
+ const deviceCount = this.configuredAccessories.size || 0;
199
+ const callsForDiscovery = 1 + (2 * deviceCount);
200
+ if (this.usageTracker) {
201
+ const reserved = await this.usageTracker.beginBatch(callsForDiscovery, 'periodicDiscovery');
202
+ if (!reserved) {
203
+ this.log.info('Skipping periodic device discovery due to API usage budget');
204
+ return;
205
+ }
145
206
  }
146
207
  }
147
- });
208
+ catch (error) {
209
+ this.log.debug('Error reserving budget for periodic discovery', error);
210
+ }
211
+ await this.deviceManager.discoverDevices();
212
+ }
213
+ }
214
+ catch (error) {
215
+ this.log.error('Error during periodic device discovery:', error);
148
216
  }
149
217
  finally {
218
+ this.handleOfflineDevices(discoveredDeviceIds);
150
219
  this.periodicDeviceDiscovering = false;
151
- this.log.debug('Ending periodic device discovery');
152
220
  this.periodicDeviceDiscoveryEmitter.emit('periodicDeviceDiscoveryComplete');
221
+ this.log.debug('Finished periodic device discovery');
153
222
  }
154
223
  }
155
- findDiscoveredDevice(discoveredDevices, platformAccessory) {
156
- this.log.debug(`Finding discovered device with Platform Accessory ${platformAccessory.displayName}`);
224
+ async processDevice(device) {
225
+ this.log.debug(`Processing device: ${device.sys_info.device_id}`);
157
226
  try {
158
- const device = discoveredDevices.find(device => device.id === platformAccessory.context.id);
159
- if (device) {
160
- this.log.debug(`Discovered device ${device.alias}`);
227
+ const now = new Date();
228
+ device.last_seen = now;
229
+ device.offline = false;
230
+ const accessory = this.findPlatformAccessory(device.sys_info.device_id);
231
+ if (accessory) {
232
+ await this.updateExistingDevice(accessory, device, now);
161
233
  }
162
234
  else {
163
- this.log.debug(`No discovered device found with Platform Accessory ${platformAccessory.displayName}`);
235
+ await this.addNewDevice(device);
164
236
  }
165
- return device;
166
237
  }
167
238
  catch (error) {
168
- this.log.error(`Error finding discovered device with Platform Accessory ${platformAccessory.displayName}: ${error}`);
169
- return undefined;
239
+ this.log.error(`Error processing device [${device.sys_info.device_id}]:`, error);
170
240
  }
171
241
  }
172
- updateAccessoryDeviceStatus(platformAccessory, device, now) {
173
- this.log.debug(`Updating Platform Accessory and HomeKit device statuses for ${platformAccessory.displayName}`);
174
- try {
175
- this.log.debug(`Setting HomeKit device ${device.alias} last seen time to now and marking as online`);
176
- device.lastSeen = now;
177
- device.offline = false;
178
- this.log.debug(`Setting Platform Accessory ${platformAccessory.displayName} last seen time to now and marking as online`);
179
- platformAccessory.context.lastSeen = now;
180
- platformAccessory.context.offline = false;
181
- this.log.debug(`Updating Platform Accessory ${platformAccessory.displayName}`);
182
- this.api.updatePlatformAccessories([platformAccessory]);
183
- this.log.debug(`Platform Accessory and HomeKit device statuses for ${platformAccessory.displayName} updated successfully`);
242
+ handleOfflineDevices(discoveredDeviceIds) {
243
+ const now = new Date();
244
+ this.configuredAccessories.forEach((accessory, uuid) => {
245
+ const deviceId = accessory.context.deviceId;
246
+ if (!deviceId) {
247
+ this.log.warn(`Accessory [${accessory.displayName}] is missing a deviceId.`);
248
+ return;
249
+ }
250
+ if (discoveredDeviceIds.has(deviceId)) {
251
+ this.log.debug(`Accessory [${accessory.displayName}] was discovered and is online.`);
252
+ this.updateAccessoryStatus(accessory, now, false);
253
+ }
254
+ else {
255
+ this.handleOfflineAccessory(accessory, uuid, now);
256
+ }
257
+ });
258
+ }
259
+ handleOfflineAccessory(accessory, uuid, now) {
260
+ const timeSinceLastSeen = now.getTime() - new Date(accessory.context.lastSeen || 0).getTime();
261
+ const offlineInterval = this.config.discoveryOptions.offlineInterval;
262
+ if (timeSinceLastSeen > offlineInterval) {
263
+ this.log.info(`Accessory [${accessory.displayName}] is offline and outside the offline interval. Removing.`);
264
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
265
+ this.configuredAccessories.delete(uuid);
184
266
  }
185
- catch (error) {
186
- this.log.error(`Error updating Platform Accessory and HomeKit device statuses for ${platformAccessory.displayName}: ${error}`);
267
+ else if (!accessory.context.offline) {
268
+ this.log.debug(`Accessory [${accessory.displayName}] is offline but within the offline interval.`);
269
+ this.updateAccessoryStatus(accessory, accessory.context.lastSeen || now, true);
187
270
  }
188
271
  }
189
- updateOrCreateHomeKitDevice(deviceId, device) {
190
- this.log.debug(`Updating or creating HomeKit device ${device.alias}`);
191
- try {
192
- if (this.homekitDevicesById.has(deviceId)) {
193
- this.log.debug(`HomeKit device ${device.alias} already exists.`);
194
- const existingDevice = this.homekitDevicesById.get(deviceId);
195
- if (existingDevice) {
196
- if (!existingDevice.isUpdating) {
197
- if (existingDevice.lock.offline === true && device.offline === false) {
198
- this.log.debug(`HomeKit device ${device.alias} was offline and is now online. ` +
199
- 'Updating device and starting polling.');
200
- existingDevice.lock = device;
201
- existingDevice.startPolling();
202
- }
203
- else {
204
- this.log.debug(`Updating existing HomeKit device ${device.alias}`);
205
- existingDevice.lock = device;
206
- }
207
- }
208
- else {
209
- this.log.debug(`HomeKit device ${device.alias} is currently updating. Skipping update.`);
210
- }
272
+ findPlatformAccessory(deviceId) {
273
+ for (const accessory of this.configuredAccessories.values()) {
274
+ if (accessory.context.deviceId === deviceId) {
275
+ return accessory;
276
+ }
277
+ }
278
+ return undefined;
279
+ }
280
+ async updateExistingDevice(accessory, device, now) {
281
+ this.log.debug(`Device [${device.sys_info.device_id}] is already configured, updating status.`);
282
+ this.updateAccessoryStatus(accessory, now, false);
283
+ const existingDevice = this.homekitDevicesById.get(device.sys_info.device_id);
284
+ if (existingDevice) {
285
+ if (!existingDevice.isUpdating) {
286
+ if (existingDevice.ttlockDevice.offline && !device.offline) {
287
+ this.log.debug(`Device [${device.sys_info.device_id}] was offline and is now online. Updating and starting polling.`);
288
+ existingDevice.ttlockDevice = device;
289
+ existingDevice.updateAfterPeriodicDiscovery();
290
+ existingDevice.startPolling();
211
291
  }
212
292
  else {
213
- this.log.error(`Failed to retrieve existing HomeKit device ${device.alias} from homekitDevicesById.`);
293
+ this.log.debug(`Updating existing HomeKit device [${device.sys_info.device_id}].`);
294
+ existingDevice.ttlockDevice = device;
295
+ existingDevice.updateAfterPeriodicDiscovery();
214
296
  }
215
297
  }
216
298
  else {
217
- this.log.debug(`HomeKit device ${device.alias} does not exist.`);
218
- this.foundDevice(device);
299
+ this.log.debug(`HomeKit device [${device.sys_info.device_id}] is currently updating. Skipping update.`);
219
300
  }
220
301
  }
221
- catch (error) {
222
- this.log.error(`Error updating or creating HomeKit device ${device.alias}: ${error}`);
302
+ else {
303
+ await this.addNewDevice(device);
223
304
  }
224
305
  }
225
- updateAccessoryStatus(platformAccessory) {
306
+ async addNewDevice(device) {
307
+ this.log.debug(`New device [${device.sys_info.device_id}] found, adding to HomeKit.`);
308
+ await this.foundDevice(device);
309
+ }
310
+ updateAccessoryStatus(accessory, lastSeen, offline) {
311
+ accessory.context.lastSeen = lastSeen;
312
+ accessory.context.offline = offline;
313
+ }
314
+ async startTTLockApi() {
315
+ this.log.debug('Starting TTLock API');
226
316
  try {
227
- this.log.debug(`Setting Platform Accessory ${platformAccessory.displayName} offline status to true`);
228
- platformAccessory.context.offline = true;
229
- this.api.updatePlatformAccessories([platformAccessory]);
230
- this.log.debug(`Platform Accessory ${platformAccessory.displayName} status updated successfully`);
317
+ await this.createAndAuthenticateApi();
318
+ this.log.debug('TTLock API process started successfully');
231
319
  }
232
320
  catch (error) {
233
- this.log.error(`Error updating Platform Accessory ${platformAccessory.displayName} status: ${error}`);
321
+ this.log.error(`Error starting TTLock API process: ${error instanceof Error ? error.message : 'Unknown error'}`);
322
+ throw error;
323
+ }
324
+ }
325
+ stopTTLockApi() {
326
+ this.log.debug('Stopping TTLock API');
327
+ if (this.ttLockApi) {
328
+ this.log.debug('TTLock API process found, attempting to kill the process');
329
+ this.ttLockApi = undefined;
330
+ this.log.debug('TTLock API process successfully killed');
331
+ }
332
+ else {
333
+ this.log.debug('No TTLock API process found to stop');
234
334
  }
235
335
  }
236
- handleOfflineAccessory(platformAccessory, uuid, now, offlineInterval) {
237
- this.log.debug(`Handling offline Platform Accessory ${platformAccessory.displayName}`);
336
+ async createAndAuthenticateApi() {
337
+ this.log.debug('Creating and authenticating TTLock API...');
238
338
  try {
239
- const homekitDevice = this.homekitDevicesById.get(platformAccessory.context.deviceId);
240
- if (homekitDevice) {
241
- const timeSinceLastSeen = now.getTime() - new Date(homekitDevice.lock.lastSeen).getTime();
242
- this.log.debug(`Time since last seen for Platform Accessory ${platformAccessory.displayName}: ${timeSinceLastSeen}ms, ` +
243
- `offline interval: ${offlineInterval}ms`);
244
- if (timeSinceLastSeen < offlineInterval) {
245
- this.log.debug(`Platform Accessory ${platformAccessory.displayName} is offline and within offline interval.`);
246
- homekitDevice.lock.offline = true;
247
- }
248
- else if (timeSinceLastSeen > offlineInterval) {
249
- this.log.info(`Platform Accessory ${platformAccessory.displayName} is offline and outside the offline interval, removing.`);
250
- this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [platformAccessory]);
251
- this.configuredAccessories.delete(uuid);
252
- this.log.debug(`Platform Accessory [${platformAccessory.displayName}] removed successfully.`);
253
- }
339
+ this.log.debug('Initializing UsageTracker...');
340
+ this.usageTracker = new UsageTracker(this.api, this.log, this.config.totalApiCallsPerMonth);
341
+ await this.usageTracker.init();
342
+ this.log.debug('UsageTracker initialized');
343
+ this.usageTracker.on('tierChanged', () => {
344
+ this.log.info('API usage tier changed — recalculating polling intervals...');
345
+ void this.reschedulePolling();
346
+ });
347
+ }
348
+ catch (err) {
349
+ this.log.error('Failed to initialize usage tracker', err);
350
+ this.usageTracker = undefined;
351
+ }
352
+ this.log.debug('Initializing TTLockApi...');
353
+ this.ttLockApi = new TTLockApi(this.log, this.config.clientId, this.config.clientSecret, this.usageTracker);
354
+ await this.ttLockApi.authenticate(this.config.username, this.config.password);
355
+ }
356
+ computeEffectivePollingInterval(deviceCount, userIntervalMs) {
357
+ if (!this.usageTracker) {
358
+ return userIntervalMs;
359
+ }
360
+ const usage = this.usageTracker.getUsage();
361
+ const daysRemaining = usage.daysRemaining ?? usage.daysInMonth;
362
+ const remainingDailyAllowance = (usage.remainingDailyAllowance !== undefined)
363
+ ? usage.remainingDailyAllowance
364
+ : Math.floor((usage.totalAllowed - usage.used) / Math.max(1, daysRemaining));
365
+ const pollingBudgetPerDay = Math.floor(remainingDailyAllowance * 0.8);
366
+ const estimatedCallsPerPoll = Math.max(1, deviceCount * 2);
367
+ const maxPollsPerDay = Math.max(1, Math.floor(pollingBudgetPerDay / estimatedCallsPerPoll));
368
+ const basePollingIntervalSec = Math.max(1, Math.floor(86400 / maxPollsPerDay));
369
+ const remainingFraction = usage.totalAllowed === 0 ? 0 : usage.remaining / usage.totalAllowed;
370
+ let multiplier = 1.0;
371
+ if (remainingFraction < 0.05) {
372
+ multiplier = 8.0;
373
+ }
374
+ else if (remainingFraction < 0.10) {
375
+ multiplier = 4.0;
376
+ }
377
+ else if (remainingFraction < 0.20) {
378
+ multiplier = 2.0;
379
+ }
380
+ else if (remainingFraction < 0.50) {
381
+ multiplier = 1.5;
382
+ }
383
+ const minPollSec = 30;
384
+ const maxPollSec = 3600;
385
+ const finalSec = Math.min(Math.max(basePollingIntervalSec * multiplier, minPollSec), maxPollSec);
386
+ const finalMs = Math.floor(finalSec * 1000);
387
+ return Math.max(userIntervalMs, finalMs);
388
+ }
389
+ async reschedulePolling() {
390
+ this.log.debug('Rescheduling polling for all devices');
391
+ for (const device of this.homekitDevicesById.values()) {
392
+ try {
393
+ await device.startPolling();
254
394
  }
255
- else if (platformAccessory.context.offline === true) {
256
- const timeSinceLastSeen = now.getTime() - new Date(platformAccessory.context.lastSeen).getTime();
257
- this.log.debug(`Time since last seen for Platform Accessory ${platformAccessory.displayName}: ${timeSinceLastSeen}ms, ` +
258
- `offline interval: ${offlineInterval}ms`);
259
- if (timeSinceLastSeen < offlineInterval) {
260
- this.log.debug(`Platform Accessory [${platformAccessory.displayName}] is offline and within offline interval.`);
261
- this.updateAccessoryStatus(platformAccessory);
262
- }
263
- else if (timeSinceLastSeen > offlineInterval) {
264
- this.log.info(`Platform Accessory ${platformAccessory.displayName} is offline and outside the offline interval, removing.`);
265
- this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [platformAccessory]);
266
- this.configuredAccessories.delete(uuid);
267
- this.log.debug(`Platform Accessory [${platformAccessory.displayName}] removed successfully.`);
268
- }
395
+ catch (err) {
396
+ this.log.debug('Error rescheduling polling for device', err);
269
397
  }
270
398
  }
271
- catch (error) {
272
- this.log.error(`Error handling offline Platform Accessory ${platformAccessory.displayName}: ${error}`);
273
- }
274
399
  }
275
- registerPlatformAccessory(platformAccessory) {
276
- this.log.debug('Registering platform platformAccessory:', platformAccessory.displayName);
277
- if (!this.configuredAccessories.has(platformAccessory.UUID)) {
278
- this.log.debug(`Platform Accessory ${platformAccessory.displayName} is not in configuredAccessories, adding it.`);
279
- this.configuredAccessories.set(platformAccessory.UUID, platformAccessory);
400
+ lsc(serviceOrCharacteristic, characteristic) {
401
+ const serviceName = serviceOrCharacteristic instanceof this.api.hap.Service
402
+ ? this.getServiceName(serviceOrCharacteristic)
403
+ : undefined;
404
+ const characteristicName = characteristic instanceof this.api.hap.Characteristic
405
+ ? this.getCharacteristicName(characteristic)
406
+ : serviceOrCharacteristic instanceof this.api.hap.Characteristic || 'UUID' in serviceOrCharacteristic
407
+ ? this.getCharacteristicName(serviceOrCharacteristic)
408
+ : undefined;
409
+ const result = `[${serviceName ? serviceName : ''}` +
410
+ `${serviceName && characteristicName ? '.' : ''}` +
411
+ `${characteristicName ? characteristicName : ''}]`;
412
+ return result;
413
+ }
414
+ getServiceName(service) {
415
+ const serviceName = lookup(this.api.hap.Service, (objectProp, value) => isObjectLike(objectProp) && 'UUID' in objectProp && objectProp.UUID === value, service.UUID);
416
+ return serviceName;
417
+ }
418
+ getCharacteristicName(characteristic) {
419
+ const name = characteristic.name;
420
+ const displayName = characteristic.displayName;
421
+ const lookupName = lookupCharacteristicNameByUUID(this.api.hap.Characteristic, characteristic.UUID);
422
+ return name ?? displayName ?? lookupName;
423
+ }
424
+ registerPlatformAccessory(accessory) {
425
+ this.log.debug('Registering platform accessory:', accessory.displayName);
426
+ if (!this.configuredAccessories.has(accessory.UUID)) {
427
+ this.log.debug(`Platform Accessory ${accessory.displayName} is not in configuredAccessories, adding it.`);
428
+ this.configuredAccessories.set(accessory.UUID, accessory);
280
429
  }
281
430
  else {
282
- this.log.debug(`Platform Accessory ${platformAccessory.displayName} is already in configuredAccessories.`);
431
+ this.log.debug(`Platform Accessory ${accessory.displayName} is already in configuredAccessories.`);
283
432
  }
284
- this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [platformAccessory]);
285
- this.log.debug(`Platform Accessory ${platformAccessory.displayName} registered with Homebridge.`);
433
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
434
+ this.log.debug(`Platform Accessory ${accessory.displayName} registered with Homebridge.`);
286
435
  }
287
- configureAccessory(platformAccessory) {
288
- this.log.debug(`Configuring Platform Accessory: [${platformAccessory.displayName}] UUID: ${platformAccessory.UUID}`);
289
- if (!platformAccessory.context.lastSeen && !platformAccessory.context.offline) {
290
- this.log.debug(`Setting initial lastSeen and offline status for Platform Accessory: [${platformAccessory.displayName}]`);
291
- platformAccessory.context.lastSeen = new Date();
292
- platformAccessory.context.offline = false;
293
- }
294
- if (platformAccessory.context.lastSeen) {
436
+ configureAccessory(accessory) {
437
+ this.log.debug(`Configuring Platform Accessory: [${accessory.displayName}] UUID: ${accessory.UUID}`);
438
+ if (!accessory.context.lastSeen && !accessory.context.offline) {
439
+ this.log.debug(`Setting initial lastSeen and offline status for Platform Accessory: [${accessory.displayName}]`);
440
+ accessory.context.lastSeen = new Date();
441
+ accessory.context.offline = false;
442
+ }
443
+ if (accessory.context.lastSeen) {
295
444
  const now = new Date();
296
- const timeSinceLastSeen = now.getTime() - new Date(platformAccessory.context.lastSeen).getTime();
445
+ const timeSinceLastSeen = now.getTime() - new Date(accessory.context.lastSeen).getTime();
297
446
  const offlineInterval = this.config.discoveryOptions.offlineInterval;
298
- this.log.debug(`Platform Accessory [${platformAccessory.displayName}] last seen ${timeSinceLastSeen}ms ago, ` +
299
- `offline interval is ${offlineInterval}ms, offline status: ${platformAccessory.context.offline}`);
300
- if (timeSinceLastSeen > offlineInterval && platformAccessory.context.offline === true) {
301
- this.log.info(`Platform Accessory [${platformAccessory.displayName}] is offline and outside the offline interval, ` +
447
+ this.log.debug(`Platform Accessory [${accessory.displayName}] last seen ${timeSinceLastSeen}ms ago, ` +
448
+ `offline interval is ${offlineInterval}ms, offline status: ${accessory.context.offline}`);
449
+ if (timeSinceLastSeen > offlineInterval && accessory.context.offline === true) {
450
+ this.log.info(`Platform Accessory [${accessory.displayName}] is offline and outside the offline interval, ` +
302
451
  'moving to offlineAccessories');
303
- this.configuredAccessories.delete(platformAccessory.UUID);
304
- this.offlineAccessories.set(platformAccessory.UUID, platformAccessory);
452
+ this.configuredAccessories.delete(accessory.UUID);
453
+ this.offlineAccessories.set(accessory.UUID, accessory);
305
454
  return;
306
455
  }
307
- else if (timeSinceLastSeen < offlineInterval && platformAccessory.context.offline === true) {
308
- this.log.debug(`Platform Accessory [${platformAccessory.displayName}] is offline and within offline interval.`);
456
+ else if (timeSinceLastSeen < offlineInterval && accessory.context.offline === true) {
457
+ this.log.debug(`Platform Accessory [${accessory.displayName}] is offline and within offline interval.`);
309
458
  }
310
- else if (platformAccessory.context.offline === false) {
311
- this.log.debug(`Platform Accessory [${platformAccessory.displayName}] is online, updating lastSeen time.`);
312
- platformAccessory.context.lastSeen = now;
313
- this.api.updatePlatformAccessories([platformAccessory]);
459
+ else if (accessory.context.offline === false) {
460
+ this.log.debug(`Platform Accessory [${accessory.displayName}] is online, updating lastSeen time.`);
461
+ this.updateAccessoryStatus(accessory, now, false);
314
462
  }
315
463
  }
316
- if (!this.configuredAccessories.has(platformAccessory.UUID)) {
317
- this.log.debug(`Platform Accessory [${platformAccessory.displayName}] with UUID ` +
318
- `[${platformAccessory.UUID}] is not in configuredAccessories, adding it.`);
319
- this.configuredAccessories.set(platformAccessory.UUID, platformAccessory);
464
+ if (!this.configuredAccessories.has(accessory.UUID)) {
465
+ this.log.debug(`Platform Accessory [${accessory.displayName}] with UUID [${accessory.UUID}] ` +
466
+ 'is not in configuredAccessories, adding it.');
467
+ this.configuredAccessories.set(accessory.UUID, accessory);
320
468
  }
321
469
  else {
322
- this.log.debug(`Platform Accessory [${platformAccessory.displayName}] with UUID [${platformAccessory.UUID}] is already in configuredAccessories.`);
470
+ this.log.debug(`Platform Accessory [${accessory.displayName}] with UUID ` +
471
+ `[${accessory.UUID}] is already in configuredAccessories.`);
323
472
  }
324
473
  }
325
- getServiceName(service) {
326
- const serviceName = lookup(this.api.hap.Service, (thisKeyValue, value) => isObjectLike(thisKeyValue) && 'UUID' in thisKeyValue && thisKeyValue.UUID === value, service.UUID);
327
- return serviceName;
474
+ async foundDevice(device) {
475
+ const { sys_info: { alias: deviceAlias, device_id: deviceId } } = device;
476
+ if (!deviceId) {
477
+ this.log.error('Missing deviceId:', deviceAlias);
478
+ return;
479
+ }
480
+ if (this.homekitDevicesById.has(deviceId)) {
481
+ this.log.info(`HomeKit device already added: [${deviceAlias}] [${deviceId}]`);
482
+ return;
483
+ }
484
+ this.log.info(`Adding HomeKit device: [${deviceAlias}] [${deviceId}]`);
485
+ const homekitDevice = await this.createHomeKitDevice(device);
486
+ if (homekitDevice) {
487
+ this.homekitDevicesById.set(deviceId, homekitDevice);
488
+ this.log.debug(`HomeKit device [${deviceAlias}] [${deviceId}] successfully added`);
489
+ }
490
+ else {
491
+ this.log.error(`Failed to add HomeKit device for: [${deviceAlias}] [${deviceId}]`);
492
+ }
328
493
  }
329
- getCharacteristicName(characteristic) {
330
- const name = characteristic.name;
331
- const displayName = characteristic.displayName;
332
- const lookupName = lookupCharacteristicNameByUUID(this.api.hap.Characteristic, characteristic.UUID);
333
- return name ?? displayName ?? lookupName;
494
+ async createHomeKitDevice(ttlockDevice) {
495
+ this.log.debug('Creating HomeKit device for:', ttlockDevice.sys_info);
496
+ return await create(this, ttlockDevice);
334
497
  }
335
498
  }
336
499
  //# sourceMappingURL=platform.js.map