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.
- package/CHANGELOG.md +29 -1
- package/README.md +14 -7
- package/config.schema.json +126 -1
- package/dist/HomeKitDevice.js +194 -77
- package/dist/HomeKitHistory.js +1 -1
- package/dist/config.js +207 -0
- package/dist/devices.js +113 -0
- package/dist/index.js +2 -1
- package/dist/nexustalk.js +19 -21
- package/dist/{camera.js → plugins/camera.js} +212 -239
- package/dist/{doorbell.js → plugins/doorbell.js} +32 -30
- package/dist/plugins/floodlight.js +91 -0
- package/dist/plugins/heatlink.js +17 -0
- package/dist/{protect.js → plugins/protect.js} +24 -41
- package/dist/{tempsensor.js → plugins/tempsensor.js} +13 -17
- package/dist/{thermostat.js → plugins/thermostat.js} +424 -381
- package/dist/{weather.js → plugins/weather.js} +26 -60
- package/dist/protobuf/google/trait/product/camera.proto +1 -1
- package/dist/protobuf/googlehome/foyer.proto +0 -46
- package/dist/protobuf/nest/services/apigateway.proto +31 -1
- package/dist/protobuf/nest/trait/firmware.proto +207 -89
- package/dist/protobuf/nest/trait/hvac.proto +1052 -312
- package/dist/protobuf/nest/trait/located.proto +51 -8
- package/dist/protobuf/nest/trait/network.proto +366 -36
- package/dist/protobuf/nest/trait/occupancy.proto +145 -17
- package/dist/protobuf/nest/trait/product/protect.proto +57 -43
- package/dist/protobuf/nest/trait/resourcedirectory.proto +8 -0
- package/dist/protobuf/nest/trait/sensor.proto +7 -1
- package/dist/protobuf/nest/trait/service.proto +3 -1
- package/dist/protobuf/nest/trait/structure.proto +60 -14
- package/dist/protobuf/nest/trait/ui.proto +41 -1
- package/dist/protobuf/nest/trait/user.proto +6 -1
- package/dist/protobuf/nest/trait/voiceassistant.proto +2 -1
- package/dist/protobuf/nestlabs/eventingapi/v1.proto +20 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/protobuf/wdl.proto +18 -2
- package/dist/protobuf/weave/common.proto +2 -1
- package/dist/protobuf/weave/trait/heartbeat.proto +41 -1
- package/dist/protobuf/weave/trait/power.proto +1 -0
- package/dist/protobuf/weave/trait/security.proto +10 -1
- package/dist/streamer.js +80 -80
- package/dist/system.js +1208 -1245
- package/dist/webrtc.js +28 -23
- package/package.json +12 -12
- package/dist/floodlight.js +0 -97
package/dist/HomeKitDevice.js
CHANGED
|
@@ -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.
|
|
34
|
-
// HomeKitDevice.
|
|
35
|
-
// HomeKitDevice.
|
|
36
|
-
// HomeKitDevice.
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
static
|
|
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; //
|
|
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
|
|
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
|
|
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
|
|
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
|
|
101
|
+
// As we're missing the Homebridge entry points but have the HAP library version
|
|
91
102
|
this.hap = api;
|
|
92
103
|
|
|
93
|
-
this
|
|
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 =
|
|
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
|
|
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(
|
|
159
|
-
new RegExp(
|
|
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(
|
|
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
|
|
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
|
|
199
|
+
if (typeof this?.setupDevice === 'function') {
|
|
198
200
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
|
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
|
|
227
|
-
this?.log?.info
|
|
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
|
|
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
|
|
251
|
+
if (typeof this?.removeDevice === 'function') {
|
|
242
252
|
try {
|
|
243
|
-
this.
|
|
253
|
+
this.removeDevice();
|
|
244
254
|
} catch (error) {
|
|
245
|
-
this?.log?.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
|
|
336
|
-
this?.log?.warn
|
|
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
|
|
356
|
+
this?.log?.warn?.('Device "%s" is offline', deviceData.description);
|
|
347
357
|
}
|
|
348
358
|
|
|
349
359
|
if (deviceData.online === true) {
|
|
350
|
-
this?.log?.success
|
|
360
|
+
this?.log?.success?.('Device "%s" is online', deviceData.description);
|
|
351
361
|
}
|
|
352
362
|
}
|
|
353
363
|
|
|
354
|
-
if (typeof this
|
|
364
|
+
if (typeof this?.updateDevice === 'function') {
|
|
355
365
|
try {
|
|
356
|
-
this.
|
|
366
|
+
this.updateDevice(deviceData); // Pass updated data on for accessory to process as it needs
|
|
357
367
|
} catch (error) {
|
|
358
|
-
this?.log?.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
|
|
424
|
+
if (typeof this?.messageDevice === 'function') {
|
|
423
425
|
try {
|
|
424
|
-
this.
|
|
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;
|
package/dist/HomeKitHistory.js
CHANGED
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 };
|