homebridge-nest-accfactory 0.2.9 → 0.3.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 (45) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +14 -7
  3. package/config.schema.json +126 -1
  4. package/dist/HomeKitDevice.js +194 -77
  5. package/dist/HomeKitHistory.js +1 -1
  6. package/dist/config.js +207 -0
  7. package/dist/devices.js +113 -0
  8. package/dist/index.js +2 -1
  9. package/dist/nexustalk.js +19 -21
  10. package/dist/{camera.js → plugins/camera.js} +212 -239
  11. package/dist/{doorbell.js → plugins/doorbell.js} +32 -30
  12. package/dist/plugins/floodlight.js +91 -0
  13. package/dist/plugins/heatlink.js +17 -0
  14. package/dist/{protect.js → plugins/protect.js} +24 -41
  15. package/dist/{tempsensor.js → plugins/tempsensor.js} +13 -17
  16. package/dist/{thermostat.js → plugins/thermostat.js} +424 -381
  17. package/dist/{weather.js → plugins/weather.js} +26 -60
  18. package/dist/protobuf/google/trait/product/camera.proto +1 -1
  19. package/dist/protobuf/googlehome/foyer.proto +0 -46
  20. package/dist/protobuf/nest/services/apigateway.proto +31 -1
  21. package/dist/protobuf/nest/trait/firmware.proto +207 -89
  22. package/dist/protobuf/nest/trait/hvac.proto +1052 -312
  23. package/dist/protobuf/nest/trait/located.proto +51 -8
  24. package/dist/protobuf/nest/trait/network.proto +366 -36
  25. package/dist/protobuf/nest/trait/occupancy.proto +145 -17
  26. package/dist/protobuf/nest/trait/product/protect.proto +57 -43
  27. package/dist/protobuf/nest/trait/resourcedirectory.proto +8 -0
  28. package/dist/protobuf/nest/trait/sensor.proto +7 -1
  29. package/dist/protobuf/nest/trait/service.proto +3 -1
  30. package/dist/protobuf/nest/trait/structure.proto +60 -14
  31. package/dist/protobuf/nest/trait/ui.proto +41 -1
  32. package/dist/protobuf/nest/trait/user.proto +6 -1
  33. package/dist/protobuf/nest/trait/voiceassistant.proto +2 -1
  34. package/dist/protobuf/nestlabs/eventingapi/v1.proto +20 -1
  35. package/dist/protobuf/root.proto +1 -0
  36. package/dist/protobuf/wdl.proto +18 -2
  37. package/dist/protobuf/weave/common.proto +2 -1
  38. package/dist/protobuf/weave/trait/heartbeat.proto +41 -1
  39. package/dist/protobuf/weave/trait/power.proto +1 -0
  40. package/dist/protobuf/weave/trait/security.proto +10 -1
  41. package/dist/streamer.js +80 -80
  42. package/dist/system.js +1208 -1245
  43. package/dist/webrtc.js +28 -23
  44. package/package.json +12 -12
  45. package/dist/floodlight.js +0 -97
@@ -27,15 +27,16 @@
27
27
  // HomeKitDevice.HOMEKITHISTORY
28
28
  // HomeKitDevice.PLUGIN_NAME
29
29
  // HomeKitDevice.PLATFORM_NAME
30
+ // HomeKitDevice.TYPE
31
+ // HomeKitDevice.VERSION
30
32
  //
31
33
  // The following functions should be overriden in your class which extends this
32
34
  //
33
- // HomeKitDevice.addServices()
34
- // HomeKitDevice.removeServices()
35
- // HomeKitDevice.updateServices(deviceData)
36
- // HomeKitDevice.messageServices(type, message)
35
+ // HomeKitDevice.setupDevice()
36
+ // HomeKitDevice.removeDevice()
37
+ // HomeKitDevice.updateDevice(deviceData)
38
+ // HomeKitDevice.messageDevice(type, message)
37
39
  //
38
- // Code version 8/10/2024
39
40
  // Mark Hulskamp
40
41
  'use strict';
41
42
 
@@ -43,75 +44,76 @@
43
44
  import crypto from 'crypto';
44
45
  import EventEmitter from 'node:events';
45
46
 
47
+ // Define constants
48
+ const HK_PIN_3_2_3 = /^\d{3}-\d{2}-\d{3}$/;
49
+ const HK_PIN_4_4 = /^\d{4}-\d{4}$/;
50
+ const MAC_ADDR = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
51
+ const LOGLEVELS = {
52
+ info: 'info',
53
+ success: 'success',
54
+ warn: 'warn',
55
+ error: 'error',
56
+ debug: 'debug',
57
+ };
58
+
46
59
  // Define our HomeKit device class
47
- export default class HomeKitDevice {
48
- static ADD = 'HomeKitDevice.add'; // Device add message
60
+ class HomeKitDevice {
49
61
  static UPDATE = 'HomeKitDevice.update'; // Device update message
50
62
  static REMOVE = 'HomeKitDevice.remove'; // Device remove message
51
63
  static SET = 'HomeKitDevice.set'; // Device set property message
52
64
  static GET = 'HomeKitDevice.get'; // Device get property message
53
- static PLUGIN_NAME = undefined; // Homebridge plugin name (override)
54
- static PLATFORM_NAME = undefined; // Homebridge platform name (override)
55
- static HISTORY = undefined; // HomeKit History object (override)
65
+
66
+ // Override this in the class which extends
67
+ static PLUGIN_NAME = undefined; // Homebridge plugin name
68
+ static PLATFORM_NAME = undefined; // Homebridge platform name
69
+ static HISTORY = undefined; // HomeKit History object
70
+ static TYPE = 'base'; // String naming type of device
71
+ static VERSION = '2025.06.12'; // Code version
56
72
 
57
73
  deviceData = {}; // The devices data we store
58
74
  historyService = undefined; // HomeKit history service
59
- accessory = undefined; // Accessory service for this device
60
- hap = undefined; // HomeKit Accessory Protocol API stub
75
+ accessory = undefined; // HomeKit accessory service for this device
76
+ hap = undefined; // HomeKit Accessory Protocol (HAP) API stub
61
77
  log = undefined; // Logging function object
62
78
  uuid = undefined; // UUID for this instance
63
79
 
64
80
  // Internal data only for this class
65
81
  #platform = undefined; // Homebridge platform api
66
82
  #eventEmitter = undefined; // Event emitter to use for comms
83
+ #postSetupDetails = []; // Use for extra output details once a device has been setup
67
84
 
68
85
  constructor(accessory, api, log, eventEmitter, deviceData) {
69
86
  // Validate the passed in logging object. We are expecting certain functions to be present
70
- if (
71
- typeof log?.info === 'function' &&
72
- typeof log?.success === 'function' &&
73
- typeof log?.warn === 'function' &&
74
- typeof log?.error === 'function' &&
75
- typeof log?.debug === 'function'
76
- ) {
87
+ if (Object.keys(LOGLEVELS).every((fn) => typeof log?.[fn] === 'function')) {
77
88
  this.log = log;
78
89
  }
79
90
 
80
- // Workout if we're running under HomeBridge or HAP-NodeJS library
91
+ // Workout if we're running under Homebridge or HAP-NodeJS library
81
92
  if (isNaN(api?.version) === false && typeof api?.hap === 'object' && api?.HAPLibraryVersion === undefined) {
82
- // We have the HomeBridge version number and hap API object
93
+ // We have the Homebridge version number and hap API object
83
94
  this.hap = api.hap;
84
95
  this.#platform = api;
85
96
 
86
- this?.log?.debug && this.log.debug('HomeKitDevice module using Homebridge backend for "%s"', deviceData?.description);
97
+ this.postSetupDetail('Homebridge backend', LOGLEVELS.debug);
87
98
  }
88
99
 
89
100
  if (typeof api?.HAPLibraryVersion === 'function' && api?.version === undefined && api?.hap === undefined) {
90
- // As we're missing the HomeBridge entry points but have the HAP library version
101
+ // As we're missing the Homebridge entry points but have the HAP library version
91
102
  this.hap = api;
92
103
 
93
- this?.log?.debug && this.log.debug('HomeKitDevice module using HAP-NodeJS library for "%s"', deviceData?.description);
104
+ this.postSetupDetail('HAP-NodeJS library', LOGLEVELS.debug);
94
105
  }
95
106
 
96
107
  // Generate UUID for this device instance
97
108
  // Will either be a random generated one or HAP generated one
98
109
  // HAP is based upon defined plugin name and devices serial number
99
- this.uuid = crypto.randomUUID();
100
- if (
101
- typeof HomeKitDevice.PLUGIN_NAME === 'string' &&
102
- HomeKitDevice.PLUGIN_NAME !== '' &&
103
- typeof deviceData.serialNumber === 'string' &&
104
- deviceData.serialNumber !== '' &&
105
- typeof this?.hap?.uuid?.generate === 'function'
106
- ) {
107
- this.uuid = this.hap.uuid.generate(HomeKitDevice.PLUGIN_NAME + '_' + deviceData.serialNumber.toUpperCase());
108
- }
110
+ this.uuid = HomeKitDevice.generateUUID(HomeKitDevice.PLUGIN_NAME, api, deviceData.serialNumber);
109
111
 
110
112
  // See if we were passed in an existing accessory object or array of accessory objects
111
- // Mainly used to restore a HomeBridge cached accessory
113
+ // Mainly used to restore a Homebridge cached accessory
112
114
  if (typeof accessory === 'object' && this.#platform !== undefined) {
113
115
  if (Array.isArray(accessory) === true) {
114
- this.accessory = accessory.find((accessory) => accessory?.UUID === this.uuid);
116
+ this.accessory = accessory.find((accessory) => this?.uuid !== undefined && accessory?.UUID === this.uuid);
115
117
  }
116
118
  if (Array.isArray(accessory) === false && accessory?.UUID === this.uuid) {
117
119
  this.accessory = accessory;
@@ -155,17 +157,17 @@ export default class HomeKitDevice {
155
157
  this.deviceData.manufacturer === '' ||
156
158
  (this.#platform === undefined &&
157
159
  (typeof this.deviceData?.hkPairingCode !== 'string' ||
158
- (new RegExp(/^([0-9]{3}-[0-9]{2}-[0-9]{3})$/).test(this.deviceData.hkPairingCode) === false &&
159
- new RegExp(/^([0-9]{4}-[0-9]{4})$/).test(this.deviceData.hkPairingCode) === false) ||
160
+ (new RegExp(HK_PIN_3_2_3).test(this.deviceData.hkPairingCode) === false &&
161
+ new RegExp(HK_PIN_4_4).test(this.deviceData.hkPairingCode) === false) ||
160
162
  typeof this.deviceData?.hkUsername !== 'string' ||
161
- new RegExp(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/).test(this.deviceData.hkUsername) === false))
163
+ new RegExp(MAC_ADDR).test(this.deviceData.hkUsername) === false))
162
164
  ) {
163
165
  return;
164
166
  }
165
167
 
166
168
  // If we do not have an existing accessory object, create a new one
167
169
  if (this.accessory === undefined && this.#platform !== undefined) {
168
- // Create HomeBridge platform accessory
170
+ // Create Homebridge platform accessory
169
171
  this.accessory = new this.#platform.platformAccessory(this.deviceData.description, this.uuid);
170
172
  this.#platform.registerPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [this.accessory]);
171
173
  }
@@ -194,21 +196,29 @@ export default class HomeKitDevice {
194
196
  this.historyService = new HomeKitDevice.HISTORY(this.accessory, this.log, this.hap, {});
195
197
  }
196
198
 
197
- if (typeof this.addServices === 'function') {
199
+ if (typeof this?.setupDevice === 'function') {
198
200
  try {
199
- let postSetupDetails = await this.addServices();
200
- this?.log?.info &&
201
- this.log.info('Setup %s %s as "%s"', this.deviceData.manufacturer, this.deviceData.model, this.deviceData.description);
201
+ this.postSetupDetail('Serial number "%s"', this.deviceData.serialNumber, LOGLEVELS.debug);
202
+
203
+ await this.setupDevice();
204
+
202
205
  if (this.historyService?.EveHome !== undefined) {
203
- this?.log?.info && this.log.info(' += EveHome support as "%s"', this.historyService.EveHome.evetype);
204
- }
205
- if (typeof postSetupDetails === 'object') {
206
- postSetupDetails.forEach((output) => {
207
- this?.log?.info && this.log.info(' += %s', output);
208
- });
206
+ this.postSetupDetail('EveHome support as "%s"', this.historyService.EveHome.evetype);
209
207
  }
208
+
209
+ this?.log?.info?.('Setup %s %s as "%s"', this.deviceData.manufacturer, this.deviceData.model, this.deviceData.description);
210
+
211
+ this.#postSetupDetails.forEach((entry) => {
212
+ if (typeof entry === 'string') {
213
+ this?.log?.[LOGLEVELS.info]?.(' += %s', entry);
214
+ } else if (typeof entry?.message === 'string') {
215
+ let level =
216
+ Object.hasOwn(LOGLEVELS, entry?.level) && typeof this?.log?.[entry?.level] === 'function' ? entry.level : LOGLEVELS.info;
217
+ this?.log?.[level]?.(' += ' + entry.message, ...(Array.isArray(entry?.args) ? entry.args : []));
218
+ }
219
+ });
210
220
  } catch (error) {
211
- this?.log?.error && this.log.error('addServices call for device "%s" failed. Error was', this.deviceData.description, error);
221
+ this?.log?.error('setupDevice call for device "%s" failed. Error was', this.deviceData.description, error);
212
222
  }
213
223
  }
214
224
 
@@ -223,26 +233,26 @@ export default class HomeKitDevice {
223
233
  category: this.accessory.category,
224
234
  });
225
235
 
226
- this?.log?.info && this.log.info(' += Advertising as "%s"', this.accessory.displayName);
227
- this?.log?.info && this.log.info(' += Pairing code is "%s"', this.accessory.pincode);
236
+ this?.log?.info(' += Advertising as "%s"', this.accessory.displayName);
237
+ this?.log?.info(' += Pairing code is "%s"', this.accessory.pincode);
228
238
  }
229
-
239
+ this.#postSetupDetails = []; // Dont' need these anymore
230
240
  return this.accessory; // Return our HomeKit accessory
231
241
  }
232
242
 
233
243
  remove() {
234
- this?.log?.warn && this.log.warn('Device "%s" has been removed', this.deviceData.description);
244
+ this?.log?.warn?.('Device "%s" has been removed', this.deviceData.description);
235
245
 
236
246
  if (this.#eventEmitter !== undefined) {
237
247
  // Remove listener for 'messages'
238
248
  this.#eventEmitter.removeAllListeners(this.uuid);
239
249
  }
240
250
 
241
- if (typeof this.removeServices === 'function') {
251
+ if (typeof this?.removeDevice === 'function') {
242
252
  try {
243
- this.removeServices();
253
+ this.removeDevice();
244
254
  } catch (error) {
245
- this?.log?.error && this.log.error('removeServices call for device "%s" failed. Error was', this.deviceData.description, error);
255
+ this?.log?.error('removeDevice call for device "%s" failed. Error was', this.deviceData.description, error);
246
256
  }
247
257
  }
248
258
 
@@ -332,8 +342,8 @@ export default class HomeKitDevice {
332
342
  deviceData.serialNumber !== '' &&
333
343
  deviceData.serialNumber.toUpperCase() !== this.deviceData.serialNumber.toUpperCase()
334
344
  ) {
335
- this?.log?.warn && this.log.warn('Serial number on "%s" has changed', deviceData.description);
336
- this?.log?.warn && this.log.warn('This may cause the device to become unresponsive in HomeKit');
345
+ this?.log?.warn?.('Serial number on "%s" has changed', deviceData.description);
346
+ this?.log?.warn?.('This may cause the device to become unresponsive in HomeKit');
337
347
 
338
348
  // Update software version on the HomeKit accessory
339
349
  informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, deviceData.serialNumber);
@@ -343,19 +353,19 @@ export default class HomeKitDevice {
343
353
  if (typeof deviceData?.online === 'boolean' && deviceData.online !== this.deviceData.online) {
344
354
  // Output device online/offline status
345
355
  if (deviceData.online === false) {
346
- this?.log?.warn && this.log.warn('Device "%s" is offline', deviceData.description);
356
+ this?.log?.warn?.('Device "%s" is offline', deviceData.description);
347
357
  }
348
358
 
349
359
  if (deviceData.online === true) {
350
- this?.log?.success && this.log.success('Device "%s" is online', deviceData.description);
360
+ this?.log?.success?.('Device "%s" is online', deviceData.description);
351
361
  }
352
362
  }
353
363
 
354
- if (typeof this.updateServices === 'function') {
364
+ if (typeof this?.updateDevice === 'function') {
355
365
  try {
356
- this.updateServices(deviceData); // Pass updated data on for accessory to process as it needs
366
+ this.updateDevice(deviceData); // Pass updated data on for accessory to process as it needs
357
367
  } catch (error) {
358
- this?.log?.error && this.log.error('updateServices call for device "%s" failed. Error was', deviceData.description, error);
368
+ this?.log?.error('updateDevice call for device "%s" failed. Error was', deviceData.description, error);
359
369
  }
360
370
  }
361
371
 
@@ -397,14 +407,6 @@ export default class HomeKitDevice {
397
407
 
398
408
  #message(type, message) {
399
409
  switch (type) {
400
- case HomeKitDevice.ADD: {
401
- // Got message for device add
402
- if (typeof message?.name === 'string' && isNaN(message?.category) === false && typeof message?.history === 'boolean') {
403
- this.add(message.name, Number(message.category), message.history);
404
- }
405
- break;
406
- }
407
-
408
410
  case HomeKitDevice.UPDATE: {
409
411
  // Got some device data, so process any updates
410
412
  this.update(message, false);
@@ -419,16 +421,131 @@ export default class HomeKitDevice {
419
421
 
420
422
  default: {
421
423
  // This is not a message we know about, so pass onto accessory for it to perform any processing
422
- if (typeof this.messageServices === 'function') {
424
+ if (typeof this?.messageDevice === 'function') {
423
425
  try {
424
- this.messageServices(type, message);
426
+ this.messageDevice(type, message);
425
427
  } catch (error) {
426
- this?.log?.error &&
427
- this.log.error('messageServices call for device "%s" failed. Error was', this.deviceData.description, error);
428
+ this?.log?.error('messageDevice call for device "%s" failed. Error was', this.deviceData.description, error);
428
429
  }
429
430
  }
430
431
  break;
431
432
  }
432
433
  }
433
434
  }
435
+
436
+ addHKService(hkServiceType, name = '', subType = undefined) {
437
+ let service = undefined;
438
+
439
+ if (
440
+ hkServiceType !== undefined &&
441
+ typeof this?.accessory?.getService === 'function' &&
442
+ typeof this?.accessory?.getServiceById === 'function' &&
443
+ typeof this?.accessory?.addService === 'function'
444
+ ) {
445
+ if (subType !== undefined) {
446
+ service = this.accessory.getServiceById(hkServiceType, subType);
447
+ } else {
448
+ service = this.accessory.getService(hkServiceType);
449
+ }
450
+
451
+ if (service === undefined) {
452
+ service = this.accessory.addService(hkServiceType, name, subType);
453
+ }
454
+ }
455
+
456
+ return service;
457
+ }
458
+
459
+ addHKCharacteristic(hkService, hkCharacteristicType, { props, onSet, onGet } = {}) {
460
+ let characteristic = undefined;
461
+
462
+ if (
463
+ hkCharacteristicType !== undefined &&
464
+ typeof hkService?.getCharacteristic === 'function' &&
465
+ typeof hkService?.testCharacteristic === 'function' &&
466
+ typeof hkService?.addCharacteristic === 'function' &&
467
+ typeof hkService?.addOptionalCharacteristic === 'function'
468
+ ) {
469
+ if (hkService.testCharacteristic(hkCharacteristicType) === false) {
470
+ if (
471
+ Array.isArray(hkService?.optionalCharacteristics) &&
472
+ hkService.optionalCharacteristics.includes(hkCharacteristicType) &&
473
+ typeof hkService?.addOptionalCharacteristic === 'function'
474
+ ) {
475
+ hkService.addOptionalCharacteristic(hkCharacteristicType);
476
+ } else {
477
+ hkService.addCharacteristic(hkCharacteristicType);
478
+ }
479
+ }
480
+
481
+ characteristic = hkService.getCharacteristic(hkCharacteristicType);
482
+
483
+ // Apply optional config
484
+ if (typeof onSet === 'function') {
485
+ characteristic.onSet(onSet);
486
+ }
487
+ if (typeof onGet === 'function') {
488
+ characteristic.onGet(onGet);
489
+ }
490
+ if (typeof props === 'object' && typeof characteristic.setProps === 'function') {
491
+ characteristic.setProps(props);
492
+ }
493
+ }
494
+ return characteristic;
495
+ }
496
+
497
+ postSetupDetail(message, ...args) {
498
+ if (typeof message !== 'string' || message === '') {
499
+ return;
500
+ }
501
+
502
+ let level = 'info';
503
+ let availableLevel = Object.keys(LOGLEVELS).find((lvl) => typeof this.log?.[lvl] === 'function') || 'info';
504
+ let lastArg = args.at(-1);
505
+
506
+ if (typeof lastArg === 'string' && Object.hasOwn(LOGLEVELS, lastArg)) {
507
+ level = lastArg;
508
+ args = args.slice(0, -1);
509
+ } else {
510
+ level = availableLevel;
511
+ }
512
+
513
+ this.#postSetupDetails.push({
514
+ level,
515
+ message,
516
+ args: args.length > 0 ? args : undefined,
517
+ });
518
+ }
519
+
520
+ static generateUUID(PLUGIN_NAME, api, serialNumber) {
521
+ let hap = undefined;
522
+ let uuid = crypto.randomUUID();
523
+
524
+ // Workout if we're running under Homebridge or HAP-NodeJS library
525
+ if (isNaN(api?.version) === false && typeof api?.hap === 'object' && api?.HAPLibraryVersion === undefined) {
526
+ // We have the Homebridge version number and hap API object
527
+ hap = api.hap;
528
+ }
529
+
530
+ if (typeof api?.HAPLibraryVersion === 'function' && api?.version === undefined && api?.hap === undefined) {
531
+ // As we're missing the Homebridge entry points but have the HAP library version
532
+ hap = api;
533
+ }
534
+
535
+ if (
536
+ typeof PLUGIN_NAME === 'string' &&
537
+ PLUGIN_NAME !== '' &&
538
+ typeof serialNumber === 'string' &&
539
+ serialNumber !== '' &&
540
+ typeof hap?.uuid?.generate === 'function'
541
+ ) {
542
+ uuid = hap.uuid.generate(PLUGIN_NAME + '_' + serialNumber.toUpperCase());
543
+ }
544
+
545
+ return uuid;
546
+ }
434
547
  }
548
+
549
+ // Define exports
550
+ export { HK_PIN_3_2_3, HK_PIN_4_4, MAC_ADDR, HomeKitDevice };
551
+ export default HomeKitDevice;
@@ -10,7 +10,7 @@
10
10
  //
11
11
  // Credit to https://github.com/simont77/fakegato-history for the work on starting the EveHome comms protocol decoding
12
12
  //
13
- // Version 2025/18/01
13
+ // Version 2025/01/18
14
14
  // Mark Hulskamp
15
15
 
16
16
  // Define nodejs module requirements
package/dist/config.js ADDED
@@ -0,0 +1,207 @@
1
+ // Configuration validation and processing
2
+ // Part of homebridge-nest-accfactory
3
+ //
4
+ // Code version 2025.06.12
5
+ // Mark Hulskamp
6
+ 'use strict';
7
+
8
+ // Define nodejs module requirements
9
+ import fs from 'fs';
10
+ import path from 'node:path';
11
+ import crypto from 'node:crypto';
12
+ import process from 'node:process';
13
+ import child_process from 'node:child_process';
14
+
15
+ // Define constants
16
+ const FFMPEGVERSION = '6.0.0';
17
+ const AccountType = {
18
+ Nest: 'Nest',
19
+ Google: 'Google',
20
+ };
21
+
22
+ function processConfig(config, log) {
23
+ let options = (config.options = typeof config?.options === 'object' ? config.options : {});
24
+
25
+ options.eveHistory = config.options?.eveHistory === true;
26
+ options.weather = config.options?.weather === true;
27
+ options.hksv = config.options?.hksv === true;
28
+ options.exclude = config.options?.exclude === true;
29
+
30
+ options.elevation =
31
+ isNaN(config.options?.elevation) === false && Number(config.options.elevation) >= 0 && Number(config.options.elevation) <= 8848
32
+ ? Number(config.options.elevation)
33
+ : 0;
34
+
35
+ // Controls what APIs we use, default is to use both Nest and protobuf APIs
36
+ options.useNestAPI = config.options?.useNestAPI === true || config.options?.useNestAPI === undefined;
37
+ options.useGoogleAPI = config.options?.useGoogleAPI === true || config.options?.useGoogleAPI === undefined;
38
+
39
+ // Get configuration for max number of concurrent 'live view' streams. For HomeKit Secure Video, this will always be 1
40
+ options.maxStreams = isNaN(config.options?.maxStreams) === false ? Number(config.options.maxStreams) : 2;
41
+
42
+ // Check if a ffmpeg binary exist via a specific path in configuration OR /usr/local/bin
43
+ options.ffmpeg = {};
44
+ options.ffmpeg.debug = config.options?.ffmpegDebug === true;
45
+ options.ffmpeg.binary = path.resolve(
46
+ typeof config.options?.ffmpegPath === 'string' && config.options.ffmpegPath !== '' ? config.options.ffmpegPath : '/usr/local/bin',
47
+ );
48
+
49
+ // If the path doesn't include 'ffmpeg' on the end, we'll add it here
50
+ if (options.ffmpeg.binary.endsWith('/ffmpeg') === false) {
51
+ options.ffmpeg.binary += '/ffmpeg';
52
+ }
53
+
54
+ options.ffmpeg.version = undefined;
55
+ options.ffmpeg.libspeex = false;
56
+ options.ffmpeg.libopus = false;
57
+ options.ffmpeg.libx264 = false;
58
+ options.ffmpeg.libfdk_aac = false;
59
+
60
+ if (fs.existsSync(options.ffmpeg.binary) === false) {
61
+ // If we flag ffmpegPath as undefined, no video streaming/record support enabled for camers/doorbells
62
+ log?.warn?.('Specified ffmpeg binary "%s" was not found', options.ffmpeg.binary);
63
+ log?.warn?.('Stream video/recording from camera/doorbells will be unavailable');
64
+ options.ffmpeg.binary = undefined;
65
+ }
66
+
67
+ if (fs.existsSync(options.ffmpeg.binary) === true) {
68
+ let ffmpegProcess = child_process.spawnSync(options.ffmpeg.binary, ['-version'], {
69
+ env: process.env,
70
+ });
71
+
72
+ if (ffmpegProcess.stdout !== null) {
73
+ let stdout = ffmpegProcess.stdout.toString();
74
+
75
+ // Determine what libraries ffmpeg is compiled with
76
+ options.ffmpeg.version = stdout.match(/(?:ffmpeg version:(\d+)\.)?(?:(\d+)\.)?(?:(\d+)\.\d+)(.*?)/gim)?.[0];
77
+ options.ffmpeg.libspeex = stdout.includes('--enable-libspeex') === true;
78
+ options.ffmpeg.libopus = stdout.includes('--enable-libopus') === true;
79
+ options.ffmpeg.libx264 = stdout.includes('--enable-libx264') === true;
80
+ options.ffmpeg.libfdk_aac = stdout.includes('--enable-libfdk-aac') === true;
81
+
82
+ let versionTooOld =
83
+ options.ffmpeg.version?.localeCompare(FFMPEGVERSION, undefined, {
84
+ numeric: true,
85
+ sensitivity: 'case',
86
+ caseFirst: 'upper',
87
+ }) === -1;
88
+
89
+ if (
90
+ versionTooOld ||
91
+ options.ffmpeg.libspeex === false ||
92
+ options.ffmpeg.libopus === false ||
93
+ options.ffmpeg.libx264 === false ||
94
+ options.ffmpeg.libfdk_aac === false
95
+ ) {
96
+ log?.warn?.('ffmpeg binary "%s" does not meet the minimum support requirements', options.ffmpeg.binary);
97
+
98
+ if (versionTooOld) {
99
+ log?.warn?.('Minimum binary version is "%s", however the installed version is "%s"', FFMPEGVERSION, options.ffmpeg.version);
100
+ log?.warn?.('Stream video/recording from camera/doorbells will be unavailable');
101
+ options.ffmpeg.binary = undefined; // No ffmpeg since below min version
102
+ }
103
+
104
+ if (!options.ffmpeg.libspeex && options.ffmpeg.libx264 && options.ffmpeg.libfdk_aac) {
105
+ log?.warn?.('Missing libspeex in ffmpeg binary, talkback on certain camera/doorbells will be unavailable');
106
+ }
107
+
108
+ if (options.ffmpeg.libx264 && !options.ffmpeg.libfdk_aac && !options.ffmpeg.libopus) {
109
+ log?.warn?.('Missing libfdk_aac and libopus in ffmpeg binary, audio from camera/doorbells will be unavailable');
110
+ }
111
+
112
+ if (options.ffmpeg.libx264 && !options.ffmpeg.libfdk_aac) {
113
+ log?.warn?.('Missing libfdk_aac in ffmpeg binary, audio from camera/doorbells will be unavailable');
114
+ }
115
+
116
+ if (options.ffmpeg.libx264 && options.ffmpeg.libfdk_aac && !options.ffmpeg.libopus) {
117
+ log?.warn?.('Missing libopus in ffmpeg binary, audio (including talkback) from certain camera/doorbells will be unavailable');
118
+ }
119
+
120
+ if (!options.ffmpeg.libx264) {
121
+ log?.warn?.('Missing libx264 in ffmpeg binary, stream video/recording from camera/doorbells will be unavailable');
122
+ options.ffmpeg.binary = undefined; // No ffmpeg since we do not have all the required libraries
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ if (options.ffmpeg.binary !== undefined) {
129
+ log?.success?.('Found valid ffmpeg binary in %s', options.ffmpeg.binary);
130
+ }
131
+
132
+ // Process per device configuration(s)
133
+ if (config?.devices === undefined) {
134
+ config.devices = [];
135
+ }
136
+
137
+ if (config?.devices !== undefined && Array.isArray(config.devices) === false) {
138
+ // If the devices section is a JSON oject keyed by the devices serial number, convert to devices array object
139
+ let newDeviceArray = [];
140
+ for (const [serialNumber, props] of Object.entries(config.devices)) {
141
+ newDeviceArray.push({
142
+ serialNumber,
143
+ ...props,
144
+ });
145
+ }
146
+ config.devices = newDeviceArray;
147
+
148
+ // Alert user to changed configuration for them to update config
149
+ log?.warn?.('');
150
+ log?.warn?.('NOTICE');
151
+ log?.warn?.('> The per device configuration contains legacy options. Please review the readme at the link below');
152
+ log?.warn?.('> Consider updating your configuration file as the mapping from legacy to current per device configuration maybe removed');
153
+ log?.warn?.('> https://github.com/n0rt0nthec4t/homebridge-nest-accfactory/blob/main/src/README.md');
154
+ log?.warn?.('');
155
+ }
156
+
157
+ return config;
158
+ }
159
+
160
+ function buildConnections(config) {
161
+ let connections = {};
162
+
163
+ Object.keys(config).forEach((key) => {
164
+ let section = config[key];
165
+
166
+ if (typeof section?.access_token === 'string' && section.access_token !== '') {
167
+ let fieldTest = section?.fieldTest === true;
168
+ connections[crypto.randomUUID()] = {
169
+ name: key,
170
+ type: AccountType.Nest,
171
+ authorised: false,
172
+ access_token: section.access_token,
173
+ fieldTest,
174
+ referer: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
175
+ restAPIHost: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
176
+ cameraAPIHost: fieldTest ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
177
+ protobufAPIHost: fieldTest ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
178
+ };
179
+ }
180
+
181
+ if (
182
+ typeof section?.issuetoken === 'string' &&
183
+ section.issuetoken !== '' &&
184
+ typeof section?.cookie === 'string' &&
185
+ section.cookie !== ''
186
+ ) {
187
+ let fieldTest = section?.fieldTest === true;
188
+ connections[crypto.randomUUID()] = {
189
+ name: key,
190
+ type: AccountType.Google,
191
+ authorised: false,
192
+ issuetoken: section.issuetoken,
193
+ cookie: section.cookie,
194
+ fieldTest,
195
+ referer: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
196
+ restAPIHost: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
197
+ cameraAPIHost: fieldTest ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
198
+ protobufAPIHost: fieldTest ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
199
+ };
200
+ }
201
+ });
202
+
203
+ return connections;
204
+ }
205
+
206
+ // Define exports
207
+ export { AccountType, processConfig, buildConnections };