@homebridge-plugins/homebridge-august 3.2.2 → 3.3.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.
Files changed (46) hide show
  1. package/.github/copilot-instructions.md +200 -0
  2. package/.github/npm-version-script-esm.js +97 -0
  3. package/CHANGELOG.md +44 -9
  4. package/dist/Platform.Base.d.ts +288 -0
  5. package/dist/Platform.Base.d.ts.map +1 -0
  6. package/dist/Platform.Base.js +797 -0
  7. package/dist/Platform.Base.js.map +1 -0
  8. package/dist/Platform.HAP.d.ts +27 -0
  9. package/dist/Platform.HAP.d.ts.map +1 -0
  10. package/dist/Platform.HAP.js +68 -0
  11. package/dist/Platform.HAP.js.map +1 -0
  12. package/dist/connectivity-manager.d.ts +3 -3
  13. package/dist/connectivity-manager.d.ts.map +1 -1
  14. package/dist/connectivity-manager.js +57 -14
  15. package/dist/connectivity-manager.js.map +1 -1
  16. package/dist/devices/device.d.ts +5 -16
  17. package/dist/devices/device.d.ts.map +1 -1
  18. package/dist/devices/device.js +18 -75
  19. package/dist/devices/device.js.map +1 -1
  20. package/dist/devices/lock.d.ts +3 -3
  21. package/dist/devices/lock.d.ts.map +1 -1
  22. package/dist/devices/lock.js +41 -41
  23. package/dist/devices/lock.js.map +1 -1
  24. package/dist/index.js +4 -4
  25. package/dist/index.js.map +1 -1
  26. package/dist/package.json +10 -8
  27. package/dist/platform.matter.d.ts +18 -20
  28. package/dist/platform.matter.d.ts.map +1 -1
  29. package/dist/platform.matter.js +74 -80
  30. package/dist/platform.matter.js.map +1 -1
  31. package/docs/variables/default.html +1 -1
  32. package/package.json +10 -8
  33. package/test/device.test.ts +384 -0
  34. package/test/{platform.test.ts → platform.hap.test.ts} +23 -23
  35. package/test/platform.polling.test.ts +5 -5
  36. package/.copilot/instructions.md +0 -261
  37. package/.github/scripts/compute-version-override.cjs +0 -51
  38. package/.github/scripts/normalize-changelog-header.cjs +0 -86
  39. package/.github/scripts/npm-version-script-esm-auto.js +0 -201
  40. package/.github/scripts/resolve-version-bump-esm.js +0 -118
  41. package/dist/platform.d.ts +0 -146
  42. package/dist/platform.d.ts.map +0 -1
  43. package/dist/platform.js +0 -755
  44. package/dist/platform.js.map +0 -1
  45. package/test/connectivity-manager.test.ts +0 -590
  46. package/test/platform.matter.test.ts +0 -660
package/dist/platform.js DELETED
@@ -1,755 +0,0 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
2
- import { argv } from 'node:process';
3
- import August from 'august-yale';
4
- import { ConnectivityManager } from './connectivity-manager.js';
5
- import { LockMechanism } from './devices/lock.js';
6
- import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
7
- /**
8
- * HomebridgePlatform
9
- * This class is the main constructor for your plugin, this is where you should
10
- * parse the user config and discover/register accessories with Homebridge.
11
- */
12
- export class AugustPlatform {
13
- accessories;
14
- api;
15
- log;
16
- config;
17
- platformConfig;
18
- platformLogging;
19
- platformRefreshRate;
20
- platformPushRate;
21
- platformUpdateRate;
22
- registeringDevice;
23
- debugMode;
24
- version;
25
- // August API
26
- //
27
- // augustConfig is the current August client. The ConnectivityManager
28
- // owns the lifecycle and pushes new clients into this field via its
29
- // onClientChanged callback (set up in augustCredentials()), so existing
30
- // call sites that read this field directly continue to work. New code
31
- // should prefer this.connectivity.execute() so failures are classified
32
- // and the connectivity state machine stays in sync.
33
- augustConfig;
34
- // Connectivity is the single source of truth for the August client,
35
- // network health, and retry/backoff. Initialized lazily in
36
- // augustCredentials() — undefined until first use.
37
- connectivity;
38
- // Track LockMechanism instances by lockId so they can be properly torn
39
- // down when the accessory is unregistered (releases PubNub subscriptions).
40
- lockMechanisms = new Map();
41
- // Platform-level poll timer. Replaces the per-lock rxjs intervals that
42
- // used to live on each LockMechanism. Iterating serially across all
43
- // registered locks means one timeout per cycle on a bad network rather
44
- // than N (one per lock) — and short-circuiting on the first failure
45
- // avoids hammering the API while it's clearly unreachable.
46
- pollTimer;
47
- // Cached normalized credentials for performance
48
- normalizedCredentialsCache;
49
- constructor(log, config, api) {
50
- // Initialize
51
- this.accessories = [];
52
- this.api = api;
53
- this.log = log;
54
- // only load if configured
55
- if (!config) {
56
- return;
57
- }
58
- // Plugin options into our config variables.
59
- this.config = {
60
- platform: 'August',
61
- name: config.name,
62
- credentials: config.credentials,
63
- options: config.options,
64
- };
65
- // Plugin Configuration
66
- this.getPlatformLogSettings();
67
- this.getPlatformRateSettings();
68
- this.getPlatformConfigSettings();
69
- this.getVersion();
70
- // Finish initializing the platform
71
- this.debugLog(`Finished initializing platform: ${config.name}`);
72
- // verify the config
73
- (async () => {
74
- try {
75
- this.verifyConfig();
76
- this.debugLog('Config OK');
77
- }
78
- catch (e) {
79
- await this.errorLog(`Verify Config, Error Message: ${e.message ?? e}, Submit Bugs Here: https://bit.ly/august-bug-report`);
80
- }
81
- })();
82
- // When this event is fired it means Homebridge has restored all cached accessories from disk.
83
- // Dynamic Platform plugins should only register new accessories after this event was fired,
84
- // in order to ensure they weren't added to homebridge already. This event can also be used
85
- // to start discovery of new accessories.
86
- this.api.on('didFinishLaunching', async () => {
87
- await this.debugLog('Executed didFinishLaunching callback');
88
- // run the method to discover / register your devices as accessories
89
- if (this.config.credentials?.isValidated === false || this.config.credentials?.isValidated === undefined) {
90
- await this.debugWarnLog(`Config Credentials: ${JSON.stringify(this.config.credentials)}`);
91
- try {
92
- await this.validated();
93
- }
94
- catch (e) {
95
- this.errorLog(`Validate: ${e.message ?? e}`);
96
- }
97
- }
98
- else {
99
- await this.debugWarnLog(`Config Credentials: ${JSON.stringify(this.config.credentials)}`);
100
- try {
101
- await this.discoverDevices();
102
- this.startPolling();
103
- }
104
- catch (e) {
105
- await this.errorLog(`Validated, Discover Devices: ${e.message ?? e}`);
106
- }
107
- }
108
- });
109
- }
110
- /**
111
- * This function is invoked when homebridge restores cached accessories from disk at startup.
112
- * It should be used to setup event handlers for characteristics and update respective values.
113
- */
114
- async configureAccessory(accessory) {
115
- await this.debugLog(`Loading accessory from cache: ${accessory.displayName}`);
116
- // add the restored accessory to the accessories cache so we can track if it has already been registered
117
- this.accessories.push(accessory);
118
- }
119
- /**
120
- * Verify the config passed to the plugin is valid
121
- */
122
- async verifyConfig() {
123
- if (!this.config.credentials) {
124
- throw new Error('Missing Credentials');
125
- }
126
- else {
127
- if (!this.config.credentials.augustId) {
128
- throw new Error('Missing August ID (E-mail/Phone Number)');
129
- }
130
- if (!this.config.credentials.password) {
131
- throw new Error('Missing August Password');
132
- }
133
- }
134
- }
135
- /**
136
- * This method looks to see if session is already Validate, if not then sends a validateCode and saves the installId.
137
- * After validateCode is saved to the config user must restart homebridge
138
- * this process then looks to see if session is already validated and if the validateCode in config;
139
- * if isValidated is false then it will validate iwth the validateCode and save isValidated as true in the config.json file
140
- * will also make the validateCode undefined
141
- */
142
- async validated() {
143
- if (!this.config.credentials?.installId) {
144
- this.config.credentials.installId = this.api.hap.uuid.generate(`${this.config.credentials?.augustId}`);
145
- }
146
- await this.augustCredentials();
147
- if (!this.config.credentials?.isValidated && this.config.credentials?.validateCode) {
148
- const validateCode = this.config.credentials?.validateCode;
149
- const normalizedCredentials = await this.normalizeCredentialsForApi(this.config.credentials);
150
- const isValidated = await August.validate(normalizedCredentials, validateCode);
151
- // If validated successfully, set flag for future use, and you can now use the API
152
- this.config.credentials.isValidated = isValidated;
153
- // Clear the cache since credentials have been updated
154
- this.clearNormalizedCredentialsCache();
155
- // load in the current config
156
- const { pluginConfig, currentConfig } = await this.pluginConfig();
157
- pluginConfig.credentials.isValidated = this.config.credentials?.isValidated;
158
- if (this.config.credentials.isValidated) {
159
- pluginConfig.credentials.validateCode = undefined;
160
- }
161
- await this.debugWarnLog(`isValidated: ${pluginConfig.credentials.isValidated}`);
162
- await this.debugWarnLog(`validateCode: ${pluginConfig.credentials.validateCode}`);
163
- // save the config, ensuring we maintain pretty json
164
- writeFileSync(this.api.user.configPath(), JSON.stringify(currentConfig, null, 4));
165
- if (!isValidated) {
166
- this.debugErrorLog('Validate Code is Invalid, Please Check Your Code and Restart Homebridge.');
167
- }
168
- else {
169
- try {
170
- await this.discoverDevices();
171
- await this.debugWarnLog(`isValidated: ${this.config.credentials?.isValidated}`);
172
- }
173
- catch (e) {
174
- await this.errorLog(`Validate, Discover Devices: ${e.message ?? e}`);
175
- }
176
- }
177
- }
178
- else {
179
- // load in the current config
180
- const { pluginConfig, currentConfig } = await this.pluginConfig();
181
- // set the refresh token
182
- pluginConfig.credentials.installId = this.config.credentials?.installId;
183
- // Clear the cache since credentials have been updated
184
- this.clearNormalizedCredentialsCache();
185
- await this.debugWarnLog(`installId: ${pluginConfig.credentials.installId}`);
186
- // save the config, ensuring we maintain pretty json
187
- writeFileSync(this.api.user.configPath(), JSON.stringify(currentConfig, null, 4));
188
- // A 6-digit code will be sent to your email or phone (depending on what you used for your augustId).
189
- // Need some way to get this code from the user.
190
- const normalizedCredentials = await this.normalizeCredentialsForApi(this.config.credentials);
191
- August.authorize(normalizedCredentials);
192
- await this.warnLog('Input Your August email verification code into the validateCode config and restart Homebridge.');
193
- }
194
- }
195
- async augustCredentials() {
196
- if (!this.config.credentials) {
197
- throw new Error('Missing Credentials');
198
- }
199
- else if (this.augustConfig) {
200
- await this.debugLog('August API instance already initialized, skipping');
201
- }
202
- else {
203
- // The ConnectivityManager owns the August client and rebuilds it
204
- // automatically on auth failures and after probe-confirmed network
205
- // recovery. We hand it a credentials factory rather than a one-shot
206
- // value so rebuilds always pick up the latest normalized credentials,
207
- // and a callback so the platform's public augustConfig field stays
208
- // in sync with the manager's internal client.
209
- if (!this.connectivity) {
210
- this.connectivity = new ConnectivityManager(this.log, () => this.normalizeCredentialsForApi(this.config.credentials), (client) => { this.augustConfig = client; });
211
- }
212
- await this.connectivity.init();
213
- await this.debugLog(`August Credentials: ${JSON.stringify(this.augustConfig)}`);
214
- }
215
- }
216
- /**
217
- * Normalize credentials for August API compatibility
218
- * Handle country code variations that may cause 403 errors
219
- * Maps regional country codes to supported API endpoints
220
- */
221
- async normalizeCredentialsForApi(credentials) {
222
- if (!credentials) {
223
- throw new Error('Credentials cannot be null or undefined');
224
- }
225
- const normalizedCredentials = { ...credentials };
226
- // Country code normalization mapping for API compatibility
227
- // Some regions don't have dedicated August API endpoints and need to use US servers
228
- const countryCodeMapping = {
229
- CA: 'US', // Canada -> United States (North American region)
230
- MX: 'US', // Mexico -> United States (North American region)
231
- };
232
- const originalCountryCode = credentials.countryCode?.toUpperCase();
233
- const normalizedCountryCode = originalCountryCode ? countryCodeMapping[originalCountryCode] : undefined;
234
- // Check if user has disabled normalization via config
235
- const normalizationDisabled = this.config.options?.disableCountryCodeNormalization === true;
236
- if (originalCountryCode && normalizedCountryCode && !normalizationDisabled) {
237
- await this.debugWarnLog(`Country code normalization: ${originalCountryCode} -> ${normalizedCountryCode} for API compatibility. `
238
- + `To disable this behavior, set 'disableCountryCodeNormalization: true' in options.`);
239
- normalizedCredentials.countryCode = normalizedCountryCode;
240
- }
241
- else if (originalCountryCode && normalizedCountryCode && normalizationDisabled) {
242
- await this.debugLog(`Country code normalization disabled by config. Using original country code: ${originalCountryCode}`);
243
- }
244
- else if (originalCountryCode && !normalizedCountryCode) {
245
- await this.debugLog(`Country code ${originalCountryCode} does not require normalization.`);
246
- }
247
- return normalizedCredentials;
248
- }
249
- /**
250
- * Clear the normalized credentials cache
251
- * Should be called when credentials are updated
252
- */
253
- clearNormalizedCredentialsCache() {
254
- this.normalizedCredentialsCache = undefined;
255
- this.debugLog('Cleared normalized credentials cache');
256
- }
257
- /**
258
- * Public method to get normalized credentials for use by device classes
259
- * Uses caching to avoid repeated normalization of the same credentials
260
- */
261
- async getNormalizedCredentials() {
262
- if (!this.config.credentials) {
263
- throw new Error('Missing Credentials');
264
- }
265
- // Return cached credentials if available and credentials haven't changed
266
- if (this.normalizedCredentialsCache) {
267
- // Simple check to see if the original credentials have changed
268
- const currentCredsHash = JSON.stringify(this.config.credentials);
269
- const cachedCredsHash = JSON.stringify({ ...this.normalizedCredentialsCache, countryCode: this.config.credentials.countryCode });
270
- if (currentCredsHash === cachedCredsHash
271
- || (this.normalizedCredentialsCache.augustId === this.config.credentials.augustId
272
- && this.normalizedCredentialsCache.password === this.config.credentials.password
273
- && this.normalizedCredentialsCache.installId === this.config.credentials.installId)) {
274
- await this.debugLog('Using cached normalized credentials');
275
- return this.normalizedCredentialsCache;
276
- }
277
- }
278
- // Generate new normalized credentials and cache them
279
- this.normalizedCredentialsCache = await this.normalizeCredentialsForApi(this.config.credentials);
280
- await this.debugLog('Generated and cached new normalized credentials');
281
- return this.normalizedCredentialsCache;
282
- }
283
- /**
284
- * Start the platform-level poll cycle. Iterates registered locks
285
- * SERIALLY (not in parallel) and short-circuits on the first failure.
286
- *
287
- * Why serial:
288
- * - One outstanding request at a time on a healthy network is cheap
289
- * enough (most cycles complete in <1s for a handful of locks) and
290
- * avoids racing the August API.
291
- * - On a failing network, a parallel cycle would produce N timeouts
292
- * ~simultaneously, which is exactly the log-spam pattern the
293
- * ConnectivityManager exists to prevent. Serial means one timeout,
294
- * classified once, and the rest of the cycle aborts.
295
- *
296
- * Why short-circuit on first failure:
297
- * - If the first call fails with a network error, the manager has
298
- * already entered 'degraded' state and scheduled a probe. The
299
- * remaining N-1 calls would all hit the same dead network and
300
- * produce identical errors. Aborting the cycle is the correct
301
- * thing to do.
302
- *
303
- * Why skip when offline:
304
- * - The manager's state machine is already managing recovery via
305
- * scheduled probes. Polling on top of that just wastes API calls
306
- * and risks rate limiting.
307
- */
308
- startPolling() {
309
- const refreshSeconds = this.platformRefreshRate ?? 30;
310
- if (refreshSeconds === 0) {
311
- this.debugLog('Polling disabled (platformRefreshRate = 0)');
312
- return;
313
- }
314
- if (this.pollTimer) {
315
- // Idempotent: if discovery re-runs (e.g. after re-auth), don't
316
- // stack timers.
317
- this.debugLog('Polling already started — not restarting');
318
- return;
319
- }
320
- const tick = async () => {
321
- try {
322
- if (!this.connectivity || this.connectivity.getState() === 'offline') {
323
- // Skip this cycle. The manager's probe will signal recovery.
324
- return;
325
- }
326
- for (const lock of this.lockMechanisms.values()) {
327
- // Skip locks currently mid-push so we don't race the user's
328
- // explicit lock/unlock command with a stale poll result.
329
- if (lock.lockUpdateInProgress) {
330
- continue;
331
- }
332
- const lockId = lock.device?.lockId;
333
- if (!lockId) {
334
- continue;
335
- }
336
- const details = await this.connectivity.execute(`poll ${lockId}`, client => client.details(lockId));
337
- if (details === undefined) {
338
- // execute() returned undefined because the call failed (or
339
- // the manager went offline between iterations). The state
340
- // machine has the situation; don't keep iterating.
341
- break;
342
- }
343
- await lock.applyRefresh(details);
344
- }
345
- }
346
- catch (e) {
347
- // Belt-and-suspenders. execute() should never throw for
348
- // network errors, but if anything leaks through we want the
349
- // poll loop to stay alive.
350
- await this.errorLog(`Polling tick failed: ${e?.message ?? e}`);
351
- }
352
- finally {
353
- this.pollTimer = setTimeout(tick, refreshSeconds * 1000);
354
- }
355
- };
356
- this.infoLog(`Starting platform poll loop (refreshRate=${refreshSeconds}s)`);
357
- this.pollTimer = setTimeout(tick, refreshSeconds * 1000);
358
- }
359
- /** Stop the platform poll cycle. Called from shutdown paths. */
360
- stopPolling() {
361
- if (this.pollTimer) {
362
- clearTimeout(this.pollTimer);
363
- this.pollTimer = undefined;
364
- }
365
- }
366
- async pluginConfig() {
367
- const currentConfig = JSON.parse(readFileSync(this.api.user.configPath(), 'utf8'));
368
- // check the platforms section is an array before we do array things on it
369
- if (!Array.isArray(currentConfig.platforms)) {
370
- throw new TypeError('Cannot find platforms array in config');
371
- }
372
- // find this plugins current config
373
- const pluginConfig = currentConfig.platforms.find((x) => x.platform === PLATFORM_NAME);
374
- if (!pluginConfig) {
375
- throw new Error(`Cannot find config for ${PLATFORM_NAME} in platforms array`);
376
- }
377
- // check the .credentials is an object before doing object things with it
378
- // Note: typeof null === 'object' is true, so we need to explicitly check for null and undefined
379
- if (typeof pluginConfig.credentials !== 'object' || pluginConfig.credentials === null) {
380
- // Initialize credentials as an empty object if it doesn't exist or is null
381
- pluginConfig.credentials = {};
382
- }
383
- return { pluginConfig, currentConfig };
384
- }
385
- /**
386
- * This method is used to discover the your location and devices.
387
- */
388
- async discoverDevices() {
389
- // August Locks
390
- try {
391
- // Ensure the August API instance is initialized before processing
392
- // devices. augustCredentials() is idempotent — safe to call if
393
- // already initialized (e.g. from the validated() path).
394
- await this.augustCredentials();
395
- const normalizedCredentials = await this.normalizeCredentialsForApi(this.config.credentials);
396
- const devices = await August.details(normalizedCredentials, '');
397
- let deviceLists;
398
- if (devices.length > 1) {
399
- deviceLists = devices;
400
- await this.infoLog(`Total August Locks Found: ${deviceLists.length}`);
401
- }
402
- else {
403
- deviceLists = [devices];
404
- await this.infoLog(`Total August Locks Found: ${deviceLists.length}`);
405
- }
406
- // Filter out excluded lock IDs before processing
407
- const excludeLockIds = this.config.options?.excludeLockIds ?? [];
408
- if (excludeLockIds.length > 0) {
409
- const normalizedExcludeIds = excludeLockIds.map(id => id.toUpperCase().replace(/[^A-Z0-9]+/g, ''));
410
- const beforeCount = deviceLists.length;
411
- deviceLists = deviceLists.filter((device) => {
412
- const normalizedLockId = (device.lockId ?? device.LockId ?? '').toUpperCase().replace(/[^A-Z0-9]+/g, '');
413
- return !normalizedExcludeIds.includes(normalizedLockId);
414
- });
415
- const excludedCount = beforeCount - deviceLists.length;
416
- if (excludedCount > 0) {
417
- await this.infoLog(`Excluded ${excludedCount} lock(s) via excludeLockIds config.`);
418
- }
419
- // Unregister cached accessories for excluded lock IDs
420
- for (const excludedId of excludeLockIds) {
421
- const uuid = this.api.hap.uuid.generate(excludedId);
422
- const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
423
- if (existingAccessory) {
424
- this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
425
- this.tearDownLockMechanism(excludedId);
426
- this.accessories = this.accessories.filter(accessory => accessory.UUID !== uuid);
427
- await this.warnLog(`Removing excluded accessory from cache: ${existingAccessory.displayName} (${excludedId})`);
428
- }
429
- }
430
- }
431
- if (!this.config.options?.devices) {
432
- await this.debugWarnLog(`August Platform Config Not Set: ${JSON.stringify(this.config.options?.devices)}`);
433
- const devices = deviceLists.map((v) => v);
434
- for (const device of devices) {
435
- if (device.configDeviceName) {
436
- device.deviceName = device.configDeviceName;
437
- }
438
- await this.debugLog(`August Devices: ${JSON.stringify(device)}`);
439
- await this.Lock(device);
440
- }
441
- }
442
- else if (this.config.options.devices) {
443
- await this.debugWarnLog(`August Platform Config Set: ${JSON.stringify(this.config.options?.devices)}`);
444
- const deviceConfigs = this.config.options?.devices;
445
- const mergeBylockId = (a1, a2) => a1.map((itm) => ({
446
- ...a2.find((item) => item.lockId.toUpperCase().replace(/[^A-Z0-9]+/g, '') === itm.lockId.toUpperCase().replace(/[^A-Z0-9]+/g, '') && item),
447
- ...itm,
448
- }));
449
- const devices = mergeBylockId(deviceLists, deviceConfigs);
450
- await this.debugLog(`August Lock(s): ${JSON.stringify(devices)}`);
451
- for (const device of devices) {
452
- if (device.configDeviceName) {
453
- device.deviceName = device.configDeviceName;
454
- }
455
- await this.debugLog(`device: ${JSON.stringify(device)}`);
456
- await this.Lock(device);
457
- }
458
- }
459
- else {
460
- await this.errorLog('August ID & Password Supplied, Issue with Auth.');
461
- }
462
- }
463
- catch (error) {
464
- // Handle 401 authentication errors specifically
465
- const errorMessage = error.message || String(error);
466
- if (errorMessage.includes('401') || errorMessage.toLowerCase().includes('unauthorized')) {
467
- await this.warnLog('Authentication session has expired or is invalid. Attempting re-authentication...');
468
- // Reset validation status to force re-authentication
469
- this.config.credentials.isValidated = false;
470
- // Update the config file to reflect the change
471
- try {
472
- const { pluginConfig, currentConfig } = await this.pluginConfig();
473
- pluginConfig.credentials.isValidated = false;
474
- writeFileSync(this.api.user.configPath(), JSON.stringify(currentConfig, null, 4));
475
- await this.debugLog('Updated config file with isValidated: false to trigger re-authentication');
476
- }
477
- catch (configError) {
478
- await this.errorLog(`Failed to update config file: ${configError.message ?? configError}`);
479
- }
480
- // Attempt re-authentication
481
- try {
482
- await this.warnLog('Initiating re-authentication process. Please check for verification code if prompted.');
483
- await this.validated();
484
- // validated() will call discoverDevices() again if successful
485
- }
486
- catch (authError) {
487
- throw new Error(`Re-authentication failed: ${authError.message ?? authError}. Please check your credentials and try restarting Homebridge.`);
488
- }
489
- }
490
- else {
491
- // Re-throw other errors with more context
492
- throw new Error(`Failed to discover devices: ${errorMessage}`);
493
- }
494
- }
495
- }
496
- async Lock(device) {
497
- const uuid = this.api.hap.uuid.generate(device.lockId);
498
- // see if an accessory with the same uuid has already been registered and restored from
499
- // the cached devices we stored in the `configureAccessory` method above
500
- const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
501
- if (existingAccessory) {
502
- // the accessory already exists
503
- if (await this.registerDevice(device)) {
504
- await this.infoLog(`Restoring existing accessory from cache: ${device.LockName}, Lock ID: ${device.lockId}`);
505
- // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
506
- existingAccessory.context.device = device;
507
- existingAccessory.displayName = device.configLockName
508
- ? await this.validateAndCleanDisplayName(device.configLockName, 'configLockName', device.configLockName)
509
- : await this.validateAndCleanDisplayName(device.LockName, 'LockName', device.LockName);
510
- existingAccessory.context.currentFirmwareVersion = device.currentFirmwareVersion;
511
- existingAccessory.context.model = device.skuNumber;
512
- existingAccessory.context.serialnumber = device.SerialNumber;
513
- existingAccessory.context.lockId = device.lockId;
514
- this.api.updatePlatformAccessories([existingAccessory]);
515
- // create the accessory handler for the restored accessory
516
- // this is imported from `platformAccessory.ts`
517
- this.lockMechanisms.set(device.lockId, new LockMechanism(this, existingAccessory, device));
518
- await this.debugLog(`Lock: ${device.LockName} (${device.lockId}) uuid: ${existingAccessory.UUID}`);
519
- }
520
- else {
521
- await this.unregisterPlatformAccessories(existingAccessory, device);
522
- }
523
- }
524
- else if (await this.registerDevice(device)) {
525
- // create a new accessory
526
- // eslint-disable-next-line new-cap
527
- const accessory = new this.api.platformAccessory(device.configLockName ?? device.LockName, uuid);
528
- // store a copy of the device object in the `accessory.context`
529
- // the `context` property can be used to store any data about the accessory you may need
530
- accessory.context.device = device;
531
- accessory.displayName = device.configLockName
532
- ? await this.validateAndCleanDisplayName(device.configLockName, 'configLockName', device.configLockName)
533
- : await this.validateAndCleanDisplayName(device.LockName, 'LockName', device.LockName);
534
- accessory.context.currentFirmwareVersion = device.currentFirmwareVersion;
535
- accessory.context.model = device.skuNumber;
536
- accessory.context.serialnumber = device.SerialNumber;
537
- accessory.context.lockId = device.lockId;
538
- // the accessory does not yet exist, so we need to create it
539
- if (!device.external) {
540
- await this.infoLog(`Adding new accessory: ${device.LockName}, Lock ID: ${device.lockId}`);
541
- }
542
- // create the accessory handler for the newly create accessory
543
- // this is imported from `platformAccessory.ts`
544
- this.lockMechanisms.set(device.lockId, new LockMechanism(this, accessory, device));
545
- await this.debugLog(`Lock: ${device.LockName} (${device.lockId}) uuid: ${accessory.UUID}`);
546
- // link the accessory to your platform
547
- await this.externalOrPlatform(device, accessory);
548
- this.accessories.push(accessory);
549
- }
550
- else {
551
- await this.debugErrorLog(`Unable to Register: ${device.LockName}, Lock ID: ${device.lockId} Check Config to see if is being Hidden.`);
552
- }
553
- }
554
- async registerDevice(device) {
555
- if (!device.hide_device && !device.homeKitEnabled) {
556
- this.registeringDevice = true;
557
- await this.debugLog(`Device: ${device.LockName} Enabled`);
558
- }
559
- else if (device.homeKitEnabled && device.overrideHomeKitEnabled) {
560
- this.registeringDevice = true;
561
- await this.debugWarnLog(`Device: ${device.LockName} HomeKit Enabled: ${device.homeKitEnabled}, `
562
- + `Override HomeKit Enabled: ${device.overrideHomeKitEnabled}`);
563
- }
564
- else if (device.homeKitEnabled && !device.overrideHomeKitEnabled) {
565
- this.registeringDevice = false;
566
- await this.debugErrorLog(`Device: ${device.LockName} HomeKit Enabled: `
567
- + `${device.homeKitEnabled}, device will not be registered. To enable, set overrideHomeKitEnabled to true.`);
568
- }
569
- else {
570
- this.registeringDevice = false;
571
- await this.debugLog(`Device: ${device.LockName} is Hidden.`);
572
- }
573
- return this.registeringDevice;
574
- }
575
- async externalOrPlatform(device, accessory) {
576
- if (device.external) {
577
- await this.infoLog(`${accessory.displayName} External Accessory Mode: ${device.external}`);
578
- await this.externalAccessory(accessory);
579
- }
580
- else {
581
- await this.debugLog(`${accessory.displayName} External Accessory Mode: ${device.external}`);
582
- this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
583
- }
584
- }
585
- async externalAccessory(accessory) {
586
- this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
587
- }
588
- async unregisterPlatformAccessories(existingAccessory, device) {
589
- // remove platform accessories when no longer present
590
- this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
591
- this.tearDownLockMechanism(device.lockId);
592
- await this.warnLog(`Removing existing accessory from cache: ${device.LockName}`);
593
- }
594
- /**
595
- * Tear down the LockMechanism for a given lockId, releasing its PubNub
596
- * subscription. Called when an accessory is unregistered (either via
597
- * explicit removal or excludeLockIds config).
598
- */
599
- tearDownLockMechanism(lockId) {
600
- const lockMechanism = this.lockMechanisms.get(lockId);
601
- if (lockMechanism) {
602
- lockMechanism.tearDownPubNubSubscription();
603
- this.lockMechanisms.delete(lockId);
604
- }
605
- }
606
- async getPlatformLogSettings() {
607
- this.debugMode = argv.includes('-D') ?? argv.includes('--debug');
608
- this.platformLogging = (this.config.options?.logging === 'debug' || this.config.options?.logging === 'standard'
609
- || this.config.options?.logging === 'none')
610
- ? this.config.options.logging
611
- : this.debugMode ? 'debugMode' : 'standard';
612
- const logging = this.config.options?.logging ? 'Platform Config' : this.debugMode ? 'debugMode' : 'Default';
613
- await this.debugLog(`Using ${logging} Logging: ${this.platformLogging}`);
614
- }
615
- async getPlatformRateSettings() {
616
- // RefreshRate
617
- this.platformRefreshRate = this.config.options?.refreshRate ? this.config.options.refreshRate : undefined;
618
- const refreshRate = this.config.options?.refreshRate ? 'Using Platform Config refreshRate' : 'Platform Config refreshRate Not Set';
619
- await this.debugLog(`${refreshRate}: ${this.platformRefreshRate}`);
620
- // UpdateRate
621
- this.platformUpdateRate = this.config.options?.updateRate ? this.config.options.updateRate : undefined;
622
- const updateRate = this.config.options?.updateRate ? 'Using Platform Config updateRate' : 'Platform Config updateRate Not Set';
623
- await this.debugLog(`${updateRate}: ${this.platformUpdateRate}`);
624
- // PushRate
625
- this.platformPushRate = this.config.options?.pushRate ? this.config.options.pushRate : undefined;
626
- const pushRate = this.config.options?.pushRate ? 'Using Platform Config pushRate' : 'Platform Config pushRate Not Set';
627
- await this.debugLog(`${pushRate}: ${this.platformPushRate}`);
628
- }
629
- async getPlatformConfigSettings() {
630
- if (this.config.options) {
631
- const platformConfig = {
632
- platform: 'August',
633
- };
634
- platformConfig.logging = this.config.options.logging ? this.config.options.logging : undefined;
635
- platformConfig.refreshRate = this.config.options.refreshRate ? this.config.options.refreshRate : undefined;
636
- platformConfig.updateRate = this.config.options.updateRate ? this.config.options.updateRate : undefined;
637
- platformConfig.pushRate = this.config.options.pushRate ? this.config.options.pushRate : undefined;
638
- if (Object.entries(platformConfig).length !== 0) {
639
- await this.debugLog(`Platform Config: ${JSON.stringify(platformConfig)}`);
640
- }
641
- this.platformConfig = platformConfig;
642
- }
643
- }
644
- /**
645
- * Asynchronously retrieves the version of the plugin from the package.json file.
646
- *
647
- * This method reads the package.json file located in the parent directory,
648
- * parses its content to extract the version, and logs the version using the debug logger.
649
- * The extracted version is then assigned to the `version` property of the class.
650
- *
651
- * @returns {Promise<void>} A promise that resolves when the version has been retrieved and logged.
652
- */
653
- async getVersion() {
654
- const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
655
- this.debugLog(`Plugin Version: ${version}`);
656
- this.version = version;
657
- }
658
- /**
659
- * Validate and clean a string value for a Name Characteristic.
660
- * @param displayName - The display name of the accessory.
661
- * @param name - The name of the characteristic.
662
- * @param value - The value to be validated and cleaned.
663
- * @returns The cleaned string value.
664
- */
665
- async validateAndCleanDisplayName(displayName, name, value) {
666
- if (this.config.options?.allowInvalidCharacters) {
667
- return value;
668
- }
669
- else {
670
- const validPattern = /^[\p{L}\p{N}][\p{L}\p{N} ']*[\p{L}\p{N}]$/u;
671
- const invalidCharsPattern = /[^\p{L}\p{N} ']/gu;
672
- const invalidStartEndPattern = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu;
673
- if (typeof value === 'string' && !validPattern.test(value)) {
674
- this.warnLog(`WARNING: The accessory '${displayName}' has an invalid '${name}' characteristic ('${value}'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.`);
675
- // Remove invalid characters
676
- if (invalidCharsPattern.test(value)) {
677
- const before = value;
678
- this.warnLog(`Removing invalid characters from '${name}' characteristic, if you feel this is incorrect, please enable \'allowInvalidCharacter\' in the config to allow all characters`);
679
- value = value.replace(invalidCharsPattern, '');
680
- this.warnLog(`${name} Before: '${before}' After: '${value}'`);
681
- }
682
- // Ensure it starts and ends with an alphanumeric character
683
- if (invalidStartEndPattern.test(value)) {
684
- const before = value;
685
- this.warnLog(`Removing invalid starting or ending characters from '${name}' characteristic, if you feel this is incorrect, please enable \'allowInvalidCharacter\' in the config to allow all characters`);
686
- value = value.replace(invalidStartEndPattern, '');
687
- this.warnLog(`${name} Before: '${before}' After: '${value}'`);
688
- }
689
- }
690
- return value;
691
- }
692
- }
693
- /**
694
- * If device level logging is turned on, log to log.warn
695
- * Otherwise send debug logs to log.debug
696
- */
697
- async infoLog(...log) {
698
- if (await this.enablingPlatformLogging()) {
699
- this.log.info(String(...log));
700
- }
701
- }
702
- async successLog(...log) {
703
- if (await this.enablingPlatformLogging()) {
704
- this.log.success(String(...log));
705
- }
706
- }
707
- async debugSuccessLog(...log) {
708
- if (await this.enablingPlatformLogging()) {
709
- if (await this.loggingIsDebug()) {
710
- this.log.success('[DEBUG]', String(...log));
711
- }
712
- }
713
- }
714
- async warnLog(...log) {
715
- if (await this.enablingPlatformLogging()) {
716
- this.log.warn(String(...log));
717
- }
718
- }
719
- async debugWarnLog(...log) {
720
- if (await this.enablingPlatformLogging()) {
721
- if (await this.loggingIsDebug()) {
722
- this.log.warn('[DEBUG]', String(...log));
723
- }
724
- }
725
- }
726
- async errorLog(...log) {
727
- if (await this.enablingPlatformLogging()) {
728
- this.log.error(String(...log));
729
- }
730
- }
731
- async debugErrorLog(...log) {
732
- if (await this.enablingPlatformLogging()) {
733
- if (await this.loggingIsDebug()) {
734
- this.log.error('[DEBUG]', String(...log));
735
- }
736
- }
737
- }
738
- async debugLog(...log) {
739
- if (await this.enablingPlatformLogging()) {
740
- if (this.platformLogging === 'debugMode') {
741
- this.log.debug(String(...log));
742
- }
743
- else if (this.platformLogging === 'debug') {
744
- this.log.info('[DEBUG]', String(...log));
745
- }
746
- }
747
- }
748
- async loggingIsDebug() {
749
- return this.platformLogging === 'debugMode' || this.platformLogging === 'debug';
750
- }
751
- async enablingPlatformLogging() {
752
- return this.platformLogging === 'debugMode' || this.platformLogging === 'debug' || this.platformLogging === 'standard';
753
- }
754
- }
755
- //# sourceMappingURL=platform.js.map