homebridge-ttlock-accesscode 1.0.1 → 2.0.0-beta.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 +5 -5
- package/config.schema.json +31 -4
- package/dist/api/ttlockApi.d.ts +9 -8
- package/dist/api/ttlockApi.js +128 -67
- package/dist/api/ttlockApi.js.map +1 -1
- package/dist/api/usageTracker.d.ts +37 -0
- package/dist/api/usageTracker.js +276 -0
- package/dist/api/usageTracker.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +23 -11
- package/dist/config.js.map +1 -1
- package/dist/devices/accessoryInformation.d.ts +3 -0
- package/dist/devices/accessoryInformation.js +28 -0
- package/dist/devices/accessoryInformation.js.map +1 -0
- package/dist/devices/baseDevice.d.ts +58 -0
- package/dist/devices/baseDevice.js +376 -0
- package/dist/devices/baseDevice.js.map +1 -0
- package/dist/devices/create.d.ts +4 -0
- package/dist/devices/create.js +27 -0
- package/dist/devices/create.js.map +1 -0
- package/dist/devices/descriptorHelpers.d.ts +5 -0
- package/dist/devices/descriptorHelpers.js +82 -0
- package/dist/devices/descriptorHelpers.js.map +1 -0
- package/dist/devices/deviceManager.d.ts +19 -0
- package/dist/devices/deviceManager.js +74 -0
- package/dist/devices/deviceManager.js.map +1 -0
- package/dist/devices/deviceTypes.d.ts +44 -0
- package/dist/devices/deviceTypes.js +2 -0
- package/dist/devices/deviceTypes.js.map +1 -0
- package/dist/devices/homekitLock.d.ts +18 -0
- package/dist/devices/homekitLock.js +191 -0
- package/dist/devices/homekitLock.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/platform.d.ts +45 -19
- package/dist/platform.js +397 -234
- package/dist/platform.js.map +1 -1
- package/dist/taskQueue.d.ts +12 -0
- package/dist/taskQueue.js +50 -0
- package/dist/taskQueue.js.map +1 -0
- package/dist/utils.d.ts +10 -2
- package/dist/utils.js +50 -5
- package/dist/utils.js.map +1 -1
- package/package.json +32 -21
- package/dist/homekitDevice.d.ts +0 -44
- package/dist/homekitDevice.js +0 -502
- package/dist/homekitDevice.js.map +0 -1
- package/dist/platformAccessoryInformation.d.ts +0 -3
- package/dist/platformAccessoryInformation.js +0 -35
- package/dist/platformAccessoryInformation.js.map +0 -1
- package/dist/types/index.d.ts +0 -39
- package/dist/types/index.js +0 -2
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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('
|
|
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
|
-
|
|
52
|
-
this.
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
121
|
+
this.log.error('Error verifying environment:', error);
|
|
122
|
+
throw error;
|
|
79
123
|
}
|
|
80
124
|
}
|
|
81
|
-
async
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.log.debug(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
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
|
-
|
|
105
|
-
this.log.
|
|
106
|
-
return;
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.log.error('An error occurred during startup:', error);
|
|
107
139
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
this.log.error(
|
|
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
|
|
185
|
+
this.log.debug('Periodic device discovery already in progress');
|
|
121
186
|
return;
|
|
122
187
|
}
|
|
123
|
-
if (
|
|
124
|
-
this.log.
|
|
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.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
156
|
-
this.log.debug(`
|
|
224
|
+
async processDevice(device) {
|
|
225
|
+
this.log.debug(`Processing device: ${device.sys_info.device_id}`);
|
|
157
226
|
try {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
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.
|
|
235
|
+
await this.addNewDevice(device);
|
|
164
236
|
}
|
|
165
|
-
return device;
|
|
166
237
|
}
|
|
167
238
|
catch (error) {
|
|
168
|
-
this.log.error(`Error
|
|
169
|
-
return undefined;
|
|
239
|
+
this.log.error(`Error processing device [${device.sys_info.device_id}]:`, error);
|
|
170
240
|
}
|
|
171
241
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
this.log.
|
|
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
|
-
|
|
190
|
-
this.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
222
|
-
this.
|
|
302
|
+
else {
|
|
303
|
+
await this.addNewDevice(device);
|
|
223
304
|
}
|
|
224
305
|
}
|
|
225
|
-
|
|
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.
|
|
228
|
-
|
|
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
|
|
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
|
-
|
|
237
|
-
this.log.debug(
|
|
336
|
+
async createAndAuthenticateApi() {
|
|
337
|
+
this.log.debug('Creating and authenticating TTLock API...');
|
|
238
338
|
try {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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 ${
|
|
431
|
+
this.log.debug(`Platform Accessory ${accessory.displayName} is already in configuredAccessories.`);
|
|
283
432
|
}
|
|
284
|
-
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
|
|
285
|
-
this.log.debug(`Platform Accessory ${
|
|
433
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
434
|
+
this.log.debug(`Platform Accessory ${accessory.displayName} registered with Homebridge.`);
|
|
286
435
|
}
|
|
287
|
-
configureAccessory(
|
|
288
|
-
this.log.debug(`Configuring Platform Accessory: [${
|
|
289
|
-
if (!
|
|
290
|
-
this.log.debug(`Setting initial lastSeen and offline status for Platform Accessory: [${
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
if (
|
|
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(
|
|
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 [${
|
|
299
|
-
`offline interval is ${offlineInterval}ms, offline status: ${
|
|
300
|
-
if (timeSinceLastSeen > offlineInterval &&
|
|
301
|
-
this.log.info(`Platform Accessory [${
|
|
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(
|
|
304
|
-
this.offlineAccessories.set(
|
|
452
|
+
this.configuredAccessories.delete(accessory.UUID);
|
|
453
|
+
this.offlineAccessories.set(accessory.UUID, accessory);
|
|
305
454
|
return;
|
|
306
455
|
}
|
|
307
|
-
else if (timeSinceLastSeen < offlineInterval &&
|
|
308
|
-
this.log.debug(`Platform Accessory [${
|
|
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 (
|
|
311
|
-
this.log.debug(`Platform Accessory [${
|
|
312
|
-
|
|
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(
|
|
317
|
-
this.log.debug(`Platform Accessory [${
|
|
318
|
-
|
|
319
|
-
this.configuredAccessories.set(
|
|
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 [${
|
|
470
|
+
this.log.debug(`Platform Accessory [${accessory.displayName}] with UUID ` +
|
|
471
|
+
`[${accessory.UUID}] is already in configuredAccessories.`);
|
|
323
472
|
}
|
|
324
473
|
}
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|