@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.
- package/.github/copilot-instructions.md +200 -0
- package/.github/npm-version-script-esm.js +97 -0
- package/CHANGELOG.md +44 -9
- package/dist/Platform.Base.d.ts +288 -0
- package/dist/Platform.Base.d.ts.map +1 -0
- package/dist/Platform.Base.js +797 -0
- package/dist/Platform.Base.js.map +1 -0
- package/dist/Platform.HAP.d.ts +27 -0
- package/dist/Platform.HAP.d.ts.map +1 -0
- package/dist/Platform.HAP.js +68 -0
- package/dist/Platform.HAP.js.map +1 -0
- package/dist/connectivity-manager.d.ts +3 -3
- package/dist/connectivity-manager.d.ts.map +1 -1
- package/dist/connectivity-manager.js +57 -14
- package/dist/connectivity-manager.js.map +1 -1
- package/dist/devices/device.d.ts +5 -16
- package/dist/devices/device.d.ts.map +1 -1
- package/dist/devices/device.js +18 -75
- package/dist/devices/device.js.map +1 -1
- package/dist/devices/lock.d.ts +3 -3
- package/dist/devices/lock.d.ts.map +1 -1
- package/dist/devices/lock.js +41 -41
- package/dist/devices/lock.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/package.json +10 -8
- package/dist/platform.matter.d.ts +18 -20
- package/dist/platform.matter.d.ts.map +1 -1
- package/dist/platform.matter.js +74 -80
- package/dist/platform.matter.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +10 -8
- package/test/device.test.ts +384 -0
- package/test/{platform.test.ts → platform.hap.test.ts} +23 -23
- package/test/platform.polling.test.ts +5 -5
- package/.copilot/instructions.md +0 -261
- package/.github/scripts/compute-version-override.cjs +0 -51
- package/.github/scripts/normalize-changelog-header.cjs +0 -86
- package/.github/scripts/npm-version-script-esm-auto.js +0 -201
- package/.github/scripts/resolve-version-bump-esm.js +0 -118
- package/dist/platform.d.ts +0 -146
- package/dist/platform.d.ts.map +0 -1
- package/dist/platform.js +0 -755
- package/dist/platform.js.map +0 -1
- package/test/connectivity-manager.test.ts +0 -590
- package/test/platform.matter.test.ts +0 -660
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { argv } from 'node:process';
|
|
3
|
+
import { PluginLogger } from '@homebridge/plugin-logger';
|
|
4
|
+
import August from 'august-yale';
|
|
5
|
+
import { ConnectivityManager } from './connectivity-manager.js';
|
|
6
|
+
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
|
|
7
|
+
/**
|
|
8
|
+
* Base platform class for August/Yale Homebridge plugin.
|
|
9
|
+
* Contains all shared logic for config, credentials, device discovery, polling, etc.
|
|
10
|
+
* Protocol-specific logic (HAP/Matter) should be implemented in subclasses.
|
|
11
|
+
*/
|
|
12
|
+
export class AugustPlatformBase {
|
|
13
|
+
/**
|
|
14
|
+
* Subscribes to PubNub push updates for a given lockId.
|
|
15
|
+
* Stores unsubscribe/teardown functions in the lockMechanisms map for unified cleanup.
|
|
16
|
+
* Calls the provided event handler on push events.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} lockId - The lock ID to subscribe to.
|
|
19
|
+
* @param {(event: any, timestamp: Date) => Promise<void>} onEvent - Handler for push events.
|
|
20
|
+
* @param {(status: any) => void} [onStatus] - Optional handler for PubNub status events.
|
|
21
|
+
* @returns {Promise<void>}
|
|
22
|
+
*/
|
|
23
|
+
async subscribePubNub(lockId, onEvent, onStatus) {
|
|
24
|
+
await this.augustCredentials();
|
|
25
|
+
const normalizedCredentials = await this.getNormalizedCredentials();
|
|
26
|
+
// Defensive: clean up any previous subscription for this lock
|
|
27
|
+
this.tearDownPubNubSubscription(lockId);
|
|
28
|
+
// Dynamically import August for compatibility
|
|
29
|
+
const AugustModule = await import('august-yale');
|
|
30
|
+
const August = AugustModule.default;
|
|
31
|
+
// Create a new August instance for PubNub (not shared with HTTP client)
|
|
32
|
+
const pubnubAugust = new August(normalizedCredentials);
|
|
33
|
+
let pubnubStatusUnsubscribe;
|
|
34
|
+
if (onStatus) {
|
|
35
|
+
pubnubStatusUnsubscribe = pubnubAugust.onPubNubStatus(onStatus);
|
|
36
|
+
}
|
|
37
|
+
const pubnubUnsubscribe = await pubnubAugust.subscribe(lockId, onEvent);
|
|
38
|
+
// Store teardown logic in the lockMechanism for this lock
|
|
39
|
+
let mechanism = this.lockMechanisms.get(lockId);
|
|
40
|
+
if (!mechanism) {
|
|
41
|
+
mechanism = { device: { lockId } };
|
|
42
|
+
this.lockMechanisms.set(lockId, mechanism);
|
|
43
|
+
}
|
|
44
|
+
mechanism.pubnubAugust = pubnubAugust;
|
|
45
|
+
mechanism.pubnubUnsubscribe = pubnubUnsubscribe;
|
|
46
|
+
mechanism.pubnubStatusUnsubscribe = pubnubStatusUnsubscribe;
|
|
47
|
+
mechanism.tearDownPubNubSubscription = () => {
|
|
48
|
+
if (mechanism.pubnubStatusUnsubscribe) {
|
|
49
|
+
try {
|
|
50
|
+
mechanism.pubnubStatusUnsubscribe();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
mechanism.pubnubStatusUnsubscribe = undefined;
|
|
56
|
+
}
|
|
57
|
+
if (mechanism.pubnubUnsubscribe) {
|
|
58
|
+
try {
|
|
59
|
+
mechanism.pubnubUnsubscribe();
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
mechanism.pubnubUnsubscribe = undefined;
|
|
65
|
+
}
|
|
66
|
+
if (mechanism.pubnubAugust) {
|
|
67
|
+
try {
|
|
68
|
+
mechanism.pubnubAugust.destroy();
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
// ignore
|
|
72
|
+
}
|
|
73
|
+
mechanism.pubnubAugust = undefined;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Tears down the PubNub subscription for a given lockId (if present).
|
|
79
|
+
* Safe to call multiple times.
|
|
80
|
+
* @param {string} lockId
|
|
81
|
+
*/
|
|
82
|
+
tearDownPubNubSubscription(lockId) {
|
|
83
|
+
const mechanism = this.lockMechanisms.get(lockId);
|
|
84
|
+
if (mechanism && typeof mechanism.tearDownPubNubSubscription === 'function') {
|
|
85
|
+
mechanism.tearDownPubNubSubscription();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
accessories;
|
|
89
|
+
api;
|
|
90
|
+
log;
|
|
91
|
+
config;
|
|
92
|
+
platformConfig;
|
|
93
|
+
platformLogging;
|
|
94
|
+
platformRefreshRate;
|
|
95
|
+
platformPushRate;
|
|
96
|
+
platformUpdateRate;
|
|
97
|
+
registeringDevice;
|
|
98
|
+
debugMode;
|
|
99
|
+
version;
|
|
100
|
+
augustConfig;
|
|
101
|
+
connectivity;
|
|
102
|
+
lockMechanisms = new Map();
|
|
103
|
+
pollTimer;
|
|
104
|
+
normalizedCredentialsCache;
|
|
105
|
+
logger;
|
|
106
|
+
constructor(log, config, api) {
|
|
107
|
+
this.accessories = [];
|
|
108
|
+
this.api = api;
|
|
109
|
+
this.log = log;
|
|
110
|
+
this.logger = new PluginLogger(log, {
|
|
111
|
+
getLoggingMode: async () => this.platformLogging || 'standard',
|
|
112
|
+
});
|
|
113
|
+
if (!config) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.config = {
|
|
117
|
+
platform: 'August',
|
|
118
|
+
name: config.name,
|
|
119
|
+
credentials: config.credentials,
|
|
120
|
+
options: config.options,
|
|
121
|
+
};
|
|
122
|
+
this.getPlatformLogSettings();
|
|
123
|
+
this.getPlatformRateSettings();
|
|
124
|
+
this.getPlatformConfigSettings();
|
|
125
|
+
this.getVersion();
|
|
126
|
+
this.logger.debug(`Finished initializing platform: ${config.name}`);
|
|
127
|
+
(async () => {
|
|
128
|
+
try {
|
|
129
|
+
await this.verifyConfig();
|
|
130
|
+
this.logger.debug('Config OK');
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
this.logger.error(`Verify Config, Error Message: ${e.message ?? e}, Submit Bugs Here: https://bit.ly/august-bug-report`);
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
}
|
|
137
|
+
// --- Device/Accessory management stubs ---
|
|
138
|
+
// Subclasses must implement protocol-specific logic
|
|
139
|
+
async Lock(device) {
|
|
140
|
+
void device; // suppress unused parameter warning
|
|
141
|
+
throw new Error('Lock() must be implemented by subclass');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Tears down resources for a lock mechanism, including PubNub subscriptions and polling if present.
|
|
145
|
+
*
|
|
146
|
+
* Removes the lock mechanism from the internal map and calls tearDownPubNubSubscription if implemented.
|
|
147
|
+
* Used by both HAP and Matter platforms. Lock mechanisms must implement tearDownPubNubSubscription for proper cleanup.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} lockId - The lock ID whose mechanism should be torn down.
|
|
150
|
+
* @returns {void}
|
|
151
|
+
*/
|
|
152
|
+
tearDownLockMechanism(lockId) {
|
|
153
|
+
const lockMechanism = this.lockMechanisms.get(lockId);
|
|
154
|
+
if (lockMechanism) {
|
|
155
|
+
if (typeof lockMechanism.tearDownPubNubSubscription === 'function') {
|
|
156
|
+
lockMechanism.tearDownPubNubSubscription();
|
|
157
|
+
}
|
|
158
|
+
this.lockMechanisms.delete(lockId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Handles restoration of cached accessories from disk during Homebridge startup.
|
|
163
|
+
*
|
|
164
|
+
* Called by Homebridge for each accessory previously registered and persisted in the cache.
|
|
165
|
+
* Adds the accessory to the platform's accessories array for tracking and future management.
|
|
166
|
+
* Subclasses should override this method to set up event handlers and update characteristic values as needed.
|
|
167
|
+
*
|
|
168
|
+
* @param {PlatformAccessory} accessory - The accessory instance restored from cache.
|
|
169
|
+
* @returns {Promise<void>} Resolves when the accessory is processed and added to the cache.
|
|
170
|
+
*/
|
|
171
|
+
async configureAccessory(accessory) {
|
|
172
|
+
await this.logger.debug(`Loading accessory from cache: ${accessory.displayName}`);
|
|
173
|
+
// add the restored accessory to the accessories cache so we can track if it has already been registered
|
|
174
|
+
this.accessories.push(accessory);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Verifies that the plugin configuration contains all required credentials.
|
|
178
|
+
*
|
|
179
|
+
* Checks for the presence of credentials, August ID, and password. Throws descriptive errors
|
|
180
|
+
* if any required field is missing. Should be called during initialization to ensure the platform
|
|
181
|
+
* is properly configured before proceeding with authentication or device discovery.
|
|
182
|
+
*
|
|
183
|
+
* @returns {Promise<void>} Resolves if the configuration is valid.
|
|
184
|
+
* @throws {Error} If credentials, August ID, or password are missing.
|
|
185
|
+
*/
|
|
186
|
+
async verifyConfig() {
|
|
187
|
+
if (!this.config.credentials) {
|
|
188
|
+
throw new Error('Missing Credentials');
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
if (!this.config.credentials.augustId) {
|
|
192
|
+
throw new Error('Missing August ID (E-mail/Phone Number)');
|
|
193
|
+
}
|
|
194
|
+
if (!this.config.credentials.password) {
|
|
195
|
+
throw new Error('Missing August Password');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Validates the August credentials and manages the Homebridge config state for authentication.
|
|
201
|
+
*
|
|
202
|
+
* - If the session is not validated and a validateCode is present, attempts to validate with the code,
|
|
203
|
+
* updates the config, and clears the validateCode on success.
|
|
204
|
+
* - If already validated or no validateCode is present, ensures installId is set and prompts the user
|
|
205
|
+
* to enter the verification code if needed.
|
|
206
|
+
* - Always updates the config file and clears the normalized credentials cache after changes.
|
|
207
|
+
* - Handles device discovery after successful validation.
|
|
208
|
+
*
|
|
209
|
+
* @returns {Promise<void>} Resolves when validation and config updates are complete.
|
|
210
|
+
* @throws {Error} If credentials are missing or validation fails unexpectedly.
|
|
211
|
+
*/
|
|
212
|
+
async validated() {
|
|
213
|
+
if (!this.config.credentials?.installId) {
|
|
214
|
+
this.config.credentials.installId = this.api.hap.uuid.generate(`${this.config.credentials?.augustId}`);
|
|
215
|
+
}
|
|
216
|
+
await this.augustCredentials();
|
|
217
|
+
if (!this.config.credentials?.isValidated && this.config.credentials?.validateCode) {
|
|
218
|
+
const validateCode = this.config.credentials?.validateCode;
|
|
219
|
+
const normalizedCredentials = await this.normalizeCredentialsForApi(this.config.credentials);
|
|
220
|
+
const isValidated = await August.validate(normalizedCredentials, validateCode);
|
|
221
|
+
// If validated successfully, set flag for future use, and you can now use the API
|
|
222
|
+
this.config.credentials.isValidated = isValidated;
|
|
223
|
+
// Clear the cache since credentials have been updated
|
|
224
|
+
this.clearNormalizedCredentialsCache();
|
|
225
|
+
// load in the current config
|
|
226
|
+
const { pluginConfig, currentConfig } = await this.pluginConfig();
|
|
227
|
+
pluginConfig.credentials.isValidated = this.config.credentials?.isValidated;
|
|
228
|
+
if (this.config.credentials.isValidated) {
|
|
229
|
+
pluginConfig.credentials.validateCode = undefined;
|
|
230
|
+
}
|
|
231
|
+
await this.logger.debugWarn(`isValidated: ${pluginConfig.credentials.isValidated}`);
|
|
232
|
+
await this.logger.debugWarn(`validateCode: ${pluginConfig.credentials.validateCode}`);
|
|
233
|
+
// save the config, ensuring we maintain pretty json
|
|
234
|
+
writeFileSync(this.api.user.configPath(), JSON.stringify(currentConfig, null, 4));
|
|
235
|
+
if (!isValidated) {
|
|
236
|
+
this.logger.debugError('Validate Code is Invalid, Please Check Your Code and Restart Homebridge.');
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
try {
|
|
240
|
+
await this.discoverDevices();
|
|
241
|
+
await this.logger.debugWarn(`isValidated: ${this.config.credentials?.isValidated}`);
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
await this.logger.error(`Validate, Discover Devices: ${e.message ?? e}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// load in the current config
|
|
250
|
+
const { pluginConfig, currentConfig } = await this.pluginConfig();
|
|
251
|
+
// set the refresh token
|
|
252
|
+
pluginConfig.credentials.installId = this.config.credentials?.installId;
|
|
253
|
+
// Clear the cache since credentials have been updated
|
|
254
|
+
this.clearNormalizedCredentialsCache();
|
|
255
|
+
await this.logger.debugWarn(`installId: ${pluginConfig.credentials.installId}`);
|
|
256
|
+
// save the config, ensuring we maintain pretty json
|
|
257
|
+
writeFileSync(this.api.user.configPath(), JSON.stringify(currentConfig, null, 4));
|
|
258
|
+
// A 6-digit code will be sent to your email or phone (depending on what you used for your augustId).
|
|
259
|
+
// Need some way to get this code from the user.
|
|
260
|
+
const normalizedCredentials = await this.normalizeCredentialsForApi(this.config.credentials);
|
|
261
|
+
August.authorize(normalizedCredentials);
|
|
262
|
+
await this.logger.warn('Input Your August email verification code into the validateCode config and restart Homebridge.');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Initializes and manages the August API credentials and connectivity manager.
|
|
267
|
+
*
|
|
268
|
+
* Ensures credentials are present, initializes the ConnectivityManager if needed,
|
|
269
|
+
* and keeps the public augustConfig field in sync with the manager's internal client.
|
|
270
|
+
* If already initialized, skips re-initialization. Handles automatic client rebuilds
|
|
271
|
+
* on authentication failures and network recovery.
|
|
272
|
+
*
|
|
273
|
+
* @returns {Promise<void>} Resolves when credentials and connectivity are initialized.
|
|
274
|
+
* @throws {Error} If credentials are missing.
|
|
275
|
+
*/
|
|
276
|
+
async augustCredentials() {
|
|
277
|
+
if (!this.config.credentials) {
|
|
278
|
+
throw new Error('Missing Credentials');
|
|
279
|
+
}
|
|
280
|
+
else if (this.augustConfig) {
|
|
281
|
+
await this.logger.debug('August API instance already initialized, skipping');
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// The ConnectivityManager owns the August client and rebuilds it
|
|
285
|
+
// automatically on auth failures and after probe-confirmed network
|
|
286
|
+
// recovery. We hand it a credentials factory rather than a one-shot
|
|
287
|
+
// value so rebuilds always pick up the latest normalized credentials,
|
|
288
|
+
// and a callback so the platform's public augustConfig field stays
|
|
289
|
+
// in sync with the manager's internal client.
|
|
290
|
+
if (!this.connectivity) {
|
|
291
|
+
this.connectivity = new ConnectivityManager(this.logger, () => this.normalizeCredentialsForApi(this.config.credentials), (client) => { this.augustConfig = client; });
|
|
292
|
+
}
|
|
293
|
+
await this.connectivity.init();
|
|
294
|
+
await this.logger.debug(`August Credentials: ${JSON.stringify(this.augustConfig)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Normalizes credentials for August API compatibility, handling country code variations.
|
|
299
|
+
*
|
|
300
|
+
* Maps regional country codes (e.g., CA, MX) to supported API endpoints (US) unless normalization is disabled in config.
|
|
301
|
+
* Logs normalization actions for debugging and API compatibility.
|
|
302
|
+
*
|
|
303
|
+
* @param {credentials} credentials - The credentials object to normalize.
|
|
304
|
+
* @returns {Promise<credentials>} Resolves with the normalized credentials object.
|
|
305
|
+
* @throws {Error} If credentials are null or undefined.
|
|
306
|
+
*/
|
|
307
|
+
async normalizeCredentialsForApi(credentials) {
|
|
308
|
+
if (!credentials) {
|
|
309
|
+
throw new Error('Credentials cannot be null or undefined');
|
|
310
|
+
}
|
|
311
|
+
const normalizedCredentials = { ...credentials };
|
|
312
|
+
// Country code normalization mapping for API compatibility
|
|
313
|
+
// Some regions don't have dedicated August API endpoints and need to use US servers
|
|
314
|
+
const countryCodeMapping = {
|
|
315
|
+
CA: 'US', // Canada -> United States (North American region)
|
|
316
|
+
MX: 'US', // Mexico -> United States (North American region)
|
|
317
|
+
};
|
|
318
|
+
const originalCountryCode = credentials.countryCode?.toUpperCase();
|
|
319
|
+
const normalizedCountryCode = originalCountryCode ? countryCodeMapping[originalCountryCode] : undefined;
|
|
320
|
+
// Check if user has disabled normalization via config
|
|
321
|
+
const normalizationDisabled = this.config.options?.disableCountryCodeNormalization === true;
|
|
322
|
+
if (originalCountryCode && normalizedCountryCode && !normalizationDisabled) {
|
|
323
|
+
await this.logger.debugWarn(`Country code normalization: ${originalCountryCode} -> ${normalizedCountryCode} for API compatibility. `
|
|
324
|
+
+ `To disable this behavior, set 'disableCountryCodeNormalization: true' in options.`);
|
|
325
|
+
normalizedCredentials.countryCode = normalizedCountryCode;
|
|
326
|
+
}
|
|
327
|
+
else if (originalCountryCode && normalizedCountryCode && normalizationDisabled) {
|
|
328
|
+
await this.logger.debug(`Country code normalization disabled by config. Using original country code: ${originalCountryCode}`);
|
|
329
|
+
}
|
|
330
|
+
else if (originalCountryCode && !normalizedCountryCode) {
|
|
331
|
+
await this.logger.debug(`Country code ${originalCountryCode} does not require normalization.`);
|
|
332
|
+
}
|
|
333
|
+
return normalizedCredentials;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Clears the cached normalized credentials.
|
|
337
|
+
*
|
|
338
|
+
* Should be called whenever credentials are updated or invalidated to ensure
|
|
339
|
+
* that subsequent API calls use fresh, normalized credentials.
|
|
340
|
+
* Logs the cache clear operation for debugging.
|
|
341
|
+
*
|
|
342
|
+
* @returns {void}
|
|
343
|
+
*/
|
|
344
|
+
clearNormalizedCredentialsCache() {
|
|
345
|
+
this.normalizedCredentialsCache = undefined;
|
|
346
|
+
this.logger.debug('Cleared normalized credentials cache');
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Returns normalized credentials for use by device classes, with caching to avoid redundant normalization.
|
|
350
|
+
*
|
|
351
|
+
* If the credentials have not changed since the last call, returns the cached normalized credentials.
|
|
352
|
+
* Otherwise, normalizes the credentials, caches them, and returns the new value.
|
|
353
|
+
*
|
|
354
|
+
* @returns {Promise<credentials>} Resolves with the normalized credentials object for API use.
|
|
355
|
+
* @throws {Error} If credentials are missing or invalid.
|
|
356
|
+
*/
|
|
357
|
+
async getNormalizedCredentials() {
|
|
358
|
+
if (!this.config.credentials) {
|
|
359
|
+
throw new Error('Missing Credentials');
|
|
360
|
+
}
|
|
361
|
+
// Return cached credentials if available and credentials haven't changed
|
|
362
|
+
if (this.normalizedCredentialsCache) {
|
|
363
|
+
// Simple check to see if the original credentials have changed
|
|
364
|
+
const currentCredsHash = JSON.stringify(this.config.credentials);
|
|
365
|
+
const cachedCredsHash = JSON.stringify({ ...this.normalizedCredentialsCache, countryCode: this.config.credentials.countryCode });
|
|
366
|
+
if (currentCredsHash === cachedCredsHash
|
|
367
|
+
|| (this.normalizedCredentialsCache.augustId === this.config.credentials.augustId
|
|
368
|
+
&& this.normalizedCredentialsCache.password === this.config.credentials.password
|
|
369
|
+
&& this.normalizedCredentialsCache.installId === this.config.credentials.installId)) {
|
|
370
|
+
await this.logger.debug('Using cached normalized credentials');
|
|
371
|
+
return this.normalizedCredentialsCache;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Generate new normalized credentials and cache them
|
|
375
|
+
this.normalizedCredentialsCache = await this.normalizeCredentialsForApi(this.config.credentials);
|
|
376
|
+
await this.logger.debug('Generated and cached new normalized credentials');
|
|
377
|
+
return this.normalizedCredentialsCache;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Starts the platform-level polling cycle for device state updates.
|
|
381
|
+
*
|
|
382
|
+
* Iterates registered locks serially (not in parallel) and short-circuits on the first failure.
|
|
383
|
+
*
|
|
384
|
+
* **Why serial:**
|
|
385
|
+
* - One outstanding request at a time on a healthy network is cheap enough (most cycles complete in <1s for a handful of locks) and avoids racing the August API.
|
|
386
|
+
* - On a failing network, a parallel cycle would produce N timeouts ~simultaneously, which is exactly the log-spam pattern the ConnectivityManager exists to prevent. Serial means one timeout, classified once, and the rest of the cycle aborts.
|
|
387
|
+
*
|
|
388
|
+
* **Why short-circuit on first failure:**
|
|
389
|
+
* - If the first call fails with a network error, the manager has already entered 'degraded' state and scheduled a probe. The remaining N-1 calls would all hit the same dead network and produce identical errors. Aborting the cycle is the correct thing to do.
|
|
390
|
+
*
|
|
391
|
+
* **Why skip when offline:**
|
|
392
|
+
* - The manager's state machine is already managing recovery via scheduled probes. Polling on top of that just wastes API calls and risks rate limiting.
|
|
393
|
+
*
|
|
394
|
+
* Idempotent: If discovery re-runs (e.g. after re-auth), does not stack timers.
|
|
395
|
+
*
|
|
396
|
+
* @returns {void}
|
|
397
|
+
*/
|
|
398
|
+
startPolling() {
|
|
399
|
+
const refreshSeconds = this.platformRefreshRate ?? 30;
|
|
400
|
+
if (refreshSeconds === 0) {
|
|
401
|
+
this.logger.debug('Polling disabled (platformRefreshRate = 0)');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (this.pollTimer) {
|
|
405
|
+
// Idempotent: if discovery re-runs (e.g. after re-auth), don't
|
|
406
|
+
// stack timers.
|
|
407
|
+
this.logger.debug('Polling already started — not restarting');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const tick = async () => {
|
|
411
|
+
try {
|
|
412
|
+
if (!this.connectivity || this.connectivity.getState() === 'offline') {
|
|
413
|
+
// Skip this cycle. The manager's probe will signal recovery.
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
for (const lock of this.lockMechanisms.values()) {
|
|
417
|
+
// Skip locks currently mid-push so we don't race the user's
|
|
418
|
+
// explicit lock/unlock command with a stale poll result.
|
|
419
|
+
if (lock.lockUpdateInProgress) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const lockId = lock.device?.lockId;
|
|
423
|
+
if (!lockId) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const details = await this.connectivity.execute(`poll ${lockId}`, client => client.details(lockId));
|
|
427
|
+
if (details === undefined) {
|
|
428
|
+
// execute() returned undefined because the call failed (or
|
|
429
|
+
// the manager went offline between iterations). The state
|
|
430
|
+
// machine has the situation; don't keep iterating.
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
await lock.applyRefresh(details);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
// Belt-and-suspenders. execute() should never throw for
|
|
438
|
+
// network errors, but if anything leaks through we want the
|
|
439
|
+
// poll loop to stay alive.
|
|
440
|
+
await this.logger.error(`Polling tick failed: ${e?.message ?? e}`);
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
this.pollTimer = setTimeout(tick, refreshSeconds * 1000);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
this.logger.info(`Starting platform poll loop (refreshRate=${refreshSeconds}s)`);
|
|
447
|
+
this.pollTimer = setTimeout(tick, refreshSeconds * 1000);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Stops the platform polling cycle for device state updates.
|
|
451
|
+
*
|
|
452
|
+
* Clears the polling timer if active, halting periodic device refreshes.
|
|
453
|
+
* Typically called during plugin shutdown or when polling is no longer needed.
|
|
454
|
+
*
|
|
455
|
+
* @returns {void}
|
|
456
|
+
*/
|
|
457
|
+
stopPolling() {
|
|
458
|
+
if (this.pollTimer) {
|
|
459
|
+
clearTimeout(this.pollTimer);
|
|
460
|
+
this.pollTimer = undefined;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Loads and returns the plugin's configuration and the full Homebridge config from disk.
|
|
465
|
+
*
|
|
466
|
+
* Reads the Homebridge config file, finds this plugin's config section, and ensures credentials is an object.
|
|
467
|
+
* Throws if the platforms array or plugin config is missing.
|
|
468
|
+
*
|
|
469
|
+
* @returns {Promise<{ pluginConfig: any, currentConfig: any }>} Resolves with the plugin's config and the full config object.
|
|
470
|
+
* @throws {TypeError|Error} If the platforms array or plugin config is missing.
|
|
471
|
+
*/
|
|
472
|
+
async pluginConfig() {
|
|
473
|
+
const currentConfig = JSON.parse(readFileSync(this.api.user.configPath(), 'utf8'));
|
|
474
|
+
// check the platforms section is an array before we do array things on it
|
|
475
|
+
if (!Array.isArray(currentConfig.platforms)) {
|
|
476
|
+
throw new TypeError('Cannot find platforms array in config');
|
|
477
|
+
}
|
|
478
|
+
// find this plugins current config
|
|
479
|
+
const pluginConfig = currentConfig.platforms.find((x) => x.platform === PLATFORM_NAME);
|
|
480
|
+
if (!pluginConfig) {
|
|
481
|
+
throw new Error(`Cannot find config for ${PLATFORM_NAME} in platforms array`);
|
|
482
|
+
}
|
|
483
|
+
// check the .credentials is an object before doing object things with it
|
|
484
|
+
// Note: typeof null === 'object' is true, so we need to explicitly check for null and undefined
|
|
485
|
+
if (typeof pluginConfig.credentials !== 'object' || pluginConfig.credentials === null) {
|
|
486
|
+
// Initialize credentials as an empty object if it doesn't exist or is null
|
|
487
|
+
pluginConfig.credentials = {};
|
|
488
|
+
}
|
|
489
|
+
return { pluginConfig, currentConfig };
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Discovers and registers August/Yale lock devices for the platform.
|
|
493
|
+
*
|
|
494
|
+
* Fetches device details from the August API using normalized credentials, applies config-based filtering,
|
|
495
|
+
* unregisters excluded accessories, merges config overrides, and registers each device via the Lock method.
|
|
496
|
+
* Handles authentication errors and triggers re-authentication if needed.
|
|
497
|
+
*
|
|
498
|
+
* @returns {Promise<void>} Resolves when all devices have been discovered and registered.
|
|
499
|
+
* @throws {Error} If device discovery or authentication fails.
|
|
500
|
+
*/
|
|
501
|
+
async discoverDevices() {
|
|
502
|
+
try {
|
|
503
|
+
await this.augustCredentials();
|
|
504
|
+
const normalizedCredentials = await this.normalizeCredentialsForApi(this.config.credentials);
|
|
505
|
+
const devices = await August.details(normalizedCredentials, '');
|
|
506
|
+
let deviceLists;
|
|
507
|
+
if (devices.length > 1) {
|
|
508
|
+
deviceLists = devices;
|
|
509
|
+
await this.logger.info(`Total August Locks Found: ${deviceLists.length}`);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
deviceLists = [devices];
|
|
513
|
+
await this.logger.info(`Total August Locks Found: ${deviceLists.length}`);
|
|
514
|
+
}
|
|
515
|
+
const excludeLockIds = this.config.options?.excludeLockIds ?? [];
|
|
516
|
+
if (excludeLockIds.length > 0) {
|
|
517
|
+
const normalizedExcludeIds = excludeLockIds.map(id => id.toUpperCase().replace(/[^A-Z0-9]+/g, ''));
|
|
518
|
+
const beforeCount = deviceLists.length;
|
|
519
|
+
deviceLists = deviceLists.filter((device) => {
|
|
520
|
+
const normalizedLockId = (device.lockId ?? device.LockId ?? '').toUpperCase().replace(/[^A-Z0-9]+/g, '');
|
|
521
|
+
return !normalizedExcludeIds.includes(normalizedLockId);
|
|
522
|
+
});
|
|
523
|
+
const excludedCount = beforeCount - deviceLists.length;
|
|
524
|
+
if (excludedCount > 0) {
|
|
525
|
+
await this.logger.info?.(`Excluded ${excludedCount} lock(s) via excludeLockIds config.`);
|
|
526
|
+
}
|
|
527
|
+
for (const excludedId of excludeLockIds) {
|
|
528
|
+
const uuid = this.api.hap.uuid.generate(excludedId);
|
|
529
|
+
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
|
|
530
|
+
if (existingAccessory) {
|
|
531
|
+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
|
|
532
|
+
this.tearDownLockMechanism?.(excludedId);
|
|
533
|
+
this.accessories = this.accessories.filter(accessory => accessory.UUID !== uuid);
|
|
534
|
+
await this.logger.warn?.(`Removing excluded accessory from cache: ${existingAccessory.displayName} (${excludedId})`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!this.config.options?.devices) {
|
|
539
|
+
await this.logger.debugWarn?.(`August Platform Config Not Set: ${JSON.stringify(this.config.options?.devices)}`);
|
|
540
|
+
const devices = deviceLists.map((v) => v);
|
|
541
|
+
for (const device of devices) {
|
|
542
|
+
if (device.configDeviceName) {
|
|
543
|
+
device.deviceName = device.configDeviceName;
|
|
544
|
+
}
|
|
545
|
+
await this.logger.debug?.(`August Devices: ${JSON.stringify(device)}`);
|
|
546
|
+
await this.Lock?.(device);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else if (this.config.options.devices) {
|
|
550
|
+
await this.logger.debugWarn?.(`August Platform Config Set: ${JSON.stringify(this.config.options?.devices)}`);
|
|
551
|
+
const deviceConfigs = this.config.options?.devices;
|
|
552
|
+
const mergeBylockId = (a1, a2) => a1.map((itm) => ({
|
|
553
|
+
...a2.find((item) => item.lockId.toUpperCase().replace(/[^A-Z0-9]+/g, '') === itm.lockId.toUpperCase().replace(/[^A-Z0-9]+/g, '') && item),
|
|
554
|
+
...itm,
|
|
555
|
+
}));
|
|
556
|
+
const devices = mergeBylockId(deviceLists, deviceConfigs);
|
|
557
|
+
await this.logger.debug?.(`August Lock(s): ${JSON.stringify(devices)}`);
|
|
558
|
+
for (const device of devices) {
|
|
559
|
+
if (device.configDeviceName) {
|
|
560
|
+
device.deviceName = device.configDeviceName;
|
|
561
|
+
}
|
|
562
|
+
await this.logger.debug?.(`device: ${JSON.stringify(device)}`);
|
|
563
|
+
await this.Lock?.(device);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
await this.logger.error?.('August ID & Password Supplied, Issue with Auth.');
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
const errorMessage = error.message || String(error);
|
|
572
|
+
if (errorMessage.includes('401') || errorMessage.toLowerCase().includes('unauthorized')) {
|
|
573
|
+
await this.logger.warn?.('Authentication session has expired or is invalid. Attempting re-authentication...');
|
|
574
|
+
this.config.credentials.isValidated = false;
|
|
575
|
+
try {
|
|
576
|
+
const { pluginConfig, currentConfig } = await this.pluginConfig();
|
|
577
|
+
pluginConfig.credentials.isValidated = false;
|
|
578
|
+
writeFileSync(this.api.user.configPath(), JSON.stringify(currentConfig, null, 4));
|
|
579
|
+
await this.logger.debug?.('Updated config file with isValidated: false to trigger re-authentication');
|
|
580
|
+
}
|
|
581
|
+
catch (configError) {
|
|
582
|
+
await this.logger.error?.(`Failed to update config file: ${configError.message ?? configError}`);
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
await this.logger.warn?.('Initiating re-authentication process. Please check for verification code if prompted.');
|
|
586
|
+
await this.validated();
|
|
587
|
+
}
|
|
588
|
+
catch (authError) {
|
|
589
|
+
throw new Error(`Re-authentication failed: ${authError.message ?? authError}. Please check your credentials and try restarting Homebridge.`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
throw new Error(`Failed to discover devices: ${errorMessage}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* --- Additional shared methods for config, logging, etc. ---
|
|
599
|
+
*/
|
|
600
|
+
/**
|
|
601
|
+
* Determines if a device should be registered based on its visibility and HomeKit enablement.
|
|
602
|
+
*
|
|
603
|
+
* This is a generic version; subclasses may override for protocol-specific logic.
|
|
604
|
+
* Sets the registeringDevice flag and logs the registration decision.
|
|
605
|
+
*
|
|
606
|
+
* @param {any} device - The device object to evaluate for registration.
|
|
607
|
+
* @returns {Promise<boolean>} Resolves to true if the device should be registered, false otherwise.
|
|
608
|
+
*/
|
|
609
|
+
async registerDevice(device) {
|
|
610
|
+
// This is a generic version; subclasses may override for protocol-specific logic
|
|
611
|
+
if (!device.hide_device && !device.homeKitEnabled) {
|
|
612
|
+
this.registeringDevice = true;
|
|
613
|
+
await this.logger.debug?.(`Device: ${device.LockName} Enabled`);
|
|
614
|
+
}
|
|
615
|
+
else if (device.homeKitEnabled && device.overrideHomeKitEnabled) {
|
|
616
|
+
this.registeringDevice = true;
|
|
617
|
+
await this.logger.debugWarn?.(`Device: ${device.LockName} HomeKit Enabled: ${device.homeKitEnabled}, `
|
|
618
|
+
+ `Override HomeKit Enabled: ${device.overrideHomeKitEnabled}`);
|
|
619
|
+
}
|
|
620
|
+
else if (device.homeKitEnabled && !device.overrideHomeKitEnabled) {
|
|
621
|
+
this.registeringDevice = false;
|
|
622
|
+
await this.logger.debugError?.(`Device: ${device.LockName} HomeKit Enabled: `
|
|
623
|
+
+ `${device.homeKitEnabled}, device will not be registered. To enable, set overrideHomeKitEnabled to true.`);
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
this.registeringDevice = false;
|
|
627
|
+
await this.logger.debug?.(`Device: ${device.LockName} is Hidden.`);
|
|
628
|
+
}
|
|
629
|
+
return this.registeringDevice;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Unregisters a platform accessory from Homebridge and removes it from the cache.
|
|
633
|
+
*
|
|
634
|
+
* Calls the Homebridge API to unregister the accessory, tears down the lock mechanism,
|
|
635
|
+
* removes the accessory from the internal cache, and logs the removal.
|
|
636
|
+
*
|
|
637
|
+
* @param {PlatformAccessory} existingAccessory - The accessory to unregister.
|
|
638
|
+
* @param {any} device - The device object (used for lockId and logging).
|
|
639
|
+
* @returns {Promise<void>} Resolves when the accessory has been unregistered and removed from cache.
|
|
640
|
+
*/
|
|
641
|
+
async unregisterPlatformAccessories(existingAccessory, device) {
|
|
642
|
+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
|
|
643
|
+
this.tearDownLockMechanism?.(device.lockId);
|
|
644
|
+
this.accessories = this.accessories.filter(accessory => accessory.UUID !== existingAccessory.UUID);
|
|
645
|
+
await this.logger.warn?.(`Removing existing accessory from cache: ${device.LockName}`);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Publishes an accessory as either an external accessory or a platform accessory based on device config.
|
|
649
|
+
*
|
|
650
|
+
* If the device config has `external` set, publishes the accessory as an external accessory and logs the mode.
|
|
651
|
+
* Otherwise, registers it as a platform accessory and logs the mode.
|
|
652
|
+
*
|
|
653
|
+
* @param {device & devicesConfig} device - The device configuration object.
|
|
654
|
+
* @param {PlatformAccessory} accessory - The accessory to publish or register.
|
|
655
|
+
* @returns {Promise<void>} Resolves when the accessory has been published or registered.
|
|
656
|
+
*/
|
|
657
|
+
async externalOrPlatform(device, accessory) {
|
|
658
|
+
if (device.external) {
|
|
659
|
+
await this.logger.info(`${accessory.displayName} External Accessory Mode: ${device.external}`);
|
|
660
|
+
await this.externalAccessory(accessory);
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
await this.logger.debug(`${accessory.displayName} External Accessory Mode: ${device.external}`);
|
|
664
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Publishes an accessory as an external accessory to Homebridge.
|
|
669
|
+
*
|
|
670
|
+
* Calls the Homebridge API to publish the provided accessory as an external accessory.
|
|
671
|
+
* This is used for accessories that should be exposed independently from the main bridge.
|
|
672
|
+
*
|
|
673
|
+
* @param {PlatformAccessory} accessory - The accessory to publish externally.
|
|
674
|
+
* @returns {Promise<void>} Resolves when the accessory has been published.
|
|
675
|
+
*/
|
|
676
|
+
async externalAccessory(accessory) {
|
|
677
|
+
this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Asynchronously determines and logs the platform logging mode based on config options and debug flags.
|
|
681
|
+
*
|
|
682
|
+
* Sets the platformLogging property to 'debug', 'standard', 'none', or 'debugMode' depending on config and CLI flags.
|
|
683
|
+
* Logs the selected logging mode for debugging purposes.
|
|
684
|
+
*
|
|
685
|
+
* @returns {Promise<void>} Resolves when the logging mode has been determined, set, and logged.
|
|
686
|
+
*/
|
|
687
|
+
async getPlatformLogSettings() {
|
|
688
|
+
this.debugMode = argv.includes('-D') ?? argv.includes('--debug');
|
|
689
|
+
this.platformLogging = (this.config.options?.logging === 'debug' || this.config.options?.logging === 'standard'
|
|
690
|
+
|| this.config.options?.logging === 'none')
|
|
691
|
+
? this.config.options.logging
|
|
692
|
+
: this.debugMode ? 'debugMode' : 'standard';
|
|
693
|
+
const logging = this.config.options?.logging ? 'Platform Config' : this.debugMode ? 'debugMode' : 'Default';
|
|
694
|
+
await this.logger.debug(`Using ${logging} Logging: ${this.platformLogging}`);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Asynchronously loads and logs platform polling, update, and push rate settings from the config options.
|
|
698
|
+
*
|
|
699
|
+
* Reads refreshRate, updateRate, and pushRate from the config options, assigns them to instance properties,
|
|
700
|
+
* and logs the values for debugging purposes.
|
|
701
|
+
*
|
|
702
|
+
* @returns {Promise<void>} Resolves when the rates have been loaded, assigned, and logged.
|
|
703
|
+
*/
|
|
704
|
+
async getPlatformRateSettings() {
|
|
705
|
+
// RefreshRate
|
|
706
|
+
this.platformRefreshRate = this.config.options?.refreshRate ? this.config.options.refreshRate : undefined;
|
|
707
|
+
const refreshRate = this.config.options?.refreshRate ? 'Using Platform Config refreshRate' : 'Platform Config refreshRate Not Set';
|
|
708
|
+
await this.logger.debug(`${refreshRate}: ${this.platformRefreshRate}`);
|
|
709
|
+
// UpdateRate
|
|
710
|
+
this.platformUpdateRate = this.config.options?.updateRate ? this.config.options.updateRate : undefined;
|
|
711
|
+
const updateRate = this.config.options?.updateRate ? 'Using Platform Config updateRate' : 'Platform Config updateRate Not Set';
|
|
712
|
+
await this.logger.debug(`${updateRate}: ${this.platformUpdateRate}`);
|
|
713
|
+
// PushRate
|
|
714
|
+
this.platformPushRate = this.config.options?.pushRate ? this.config.options.pushRate : undefined;
|
|
715
|
+
const pushRate = this.config.options?.pushRate ? 'Using Platform Config pushRate' : 'Platform Config pushRate Not Set';
|
|
716
|
+
await this.logger.debug(`${pushRate}: ${this.platformPushRate}`);
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Asynchronously loads and logs platform configuration settings from the config options.
|
|
720
|
+
*
|
|
721
|
+
* Reads logging, refreshRate, updateRate, and pushRate from the config options,
|
|
722
|
+
* builds a platformConfig object, logs it if present, and assigns it to this.platformConfig.
|
|
723
|
+
*
|
|
724
|
+
* @returns {Promise<void>} Resolves when the config has been loaded, logged, and assigned.
|
|
725
|
+
*/
|
|
726
|
+
async getPlatformConfigSettings() {
|
|
727
|
+
if (this.config.options) {
|
|
728
|
+
const platformConfig = {
|
|
729
|
+
platform: 'August',
|
|
730
|
+
};
|
|
731
|
+
platformConfig.logging = this.config.options.logging ? this.config.options.logging : undefined;
|
|
732
|
+
platformConfig.refreshRate = this.config.options.refreshRate ? this.config.options.refreshRate : undefined;
|
|
733
|
+
platformConfig.updateRate = this.config.options.updateRate ? this.config.options.updateRate : undefined;
|
|
734
|
+
platformConfig.pushRate = this.config.options.pushRate ? this.config.options.pushRate : undefined;
|
|
735
|
+
if (Object.entries(platformConfig).length !== 0) {
|
|
736
|
+
await this.logger.debug(`Platform Config: ${JSON.stringify(platformConfig)}`);
|
|
737
|
+
}
|
|
738
|
+
this.platformConfig = platformConfig;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Asynchronously retrieves and logs the plugin version from package.json.
|
|
743
|
+
*
|
|
744
|
+
* Reads the version from the package.json file, logs it using logger.debug, and
|
|
745
|
+
* assigns it to the `version` property of the class instance.
|
|
746
|
+
*
|
|
747
|
+
* @returns {Promise<void>} Resolves when the version has been read, logged, and assigned.
|
|
748
|
+
*/
|
|
749
|
+
async getVersion() {
|
|
750
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
751
|
+
this.logger.debug(`Plugin Version: ${version}`);
|
|
752
|
+
this.version = version;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Asynchronously validates and cleans a display name or characteristic value for HomeKit compatibility.
|
|
756
|
+
*
|
|
757
|
+
* If invalid characters are not allowed (default), this method:
|
|
758
|
+
* - Ensures the value contains only alphanumeric, space, and apostrophe characters.
|
|
759
|
+
* - Removes any invalid characters and trims invalid start/end characters.
|
|
760
|
+
* - Logs warnings and before/after values if modifications are made.
|
|
761
|
+
* If invalid characters are allowed by config, the value is returned unchanged.
|
|
762
|
+
*
|
|
763
|
+
* @param {string} displayName - The accessory's display name (for logging context).
|
|
764
|
+
* @param {string} name - The characteristic name (for logging context).
|
|
765
|
+
* @param {string} value - The value to validate and clean.
|
|
766
|
+
* @returns {Promise<string>} The cleaned (or original) value, suitable for HomeKit.
|
|
767
|
+
*/
|
|
768
|
+
async validateAndCleanDisplayName(displayName, name, value) {
|
|
769
|
+
if (this.config.options?.allowInvalidCharacters) {
|
|
770
|
+
return value;
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
const validPattern = /^[\p{L}\p{N}][\p{L}\p{N} ']*[\p{L}\p{N}]$/u;
|
|
774
|
+
const invalidCharsPattern = /[^\p{L}\p{N} ']/gu;
|
|
775
|
+
const invalidStartEndPattern = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu;
|
|
776
|
+
if (typeof value === 'string' && !validPattern.test(value)) {
|
|
777
|
+
this.logger.warn(`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.`);
|
|
778
|
+
// Remove invalid characters
|
|
779
|
+
if (invalidCharsPattern.test(value)) {
|
|
780
|
+
const before = value;
|
|
781
|
+
this.logger.warn(`Removing invalid characters from '${name}' characteristic, if you feel this is incorrect, please enable \'allowInvalidCharacter\' in the config to allow all characters`);
|
|
782
|
+
value = value.replace(invalidCharsPattern, '');
|
|
783
|
+
this.logger.warn(`${name} Before: '${before}' After: '${value}'`);
|
|
784
|
+
}
|
|
785
|
+
// Ensure it starts and ends with an alphanumeric character
|
|
786
|
+
if (invalidStartEndPattern.test(value)) {
|
|
787
|
+
const before = value;
|
|
788
|
+
this.logger.warn(`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`);
|
|
789
|
+
value = value.replace(invalidStartEndPattern, '');
|
|
790
|
+
this.logger.warn(`${name} Before: '${before}' After: '${value}'`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return value;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
//# sourceMappingURL=Platform.Base.js.map
|