homebridge-nest-accfactory 0.0.6 → 0.2.1

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/dist/system.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Nest System communications
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 13/9/2024
4
+ // Code version 3/10/2024
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
@@ -11,7 +11,7 @@ import protobuf from 'protobufjs';
11
11
  // Define nodejs module requirements
12
12
  import EventEmitter from 'node:events';
13
13
  import { Buffer } from 'node:buffer';
14
- import { setInterval, clearInterval, setTimeout } from 'node:timers';
14
+ import { setInterval, clearInterval, setTimeout, clearTimeout } from 'node:timers';
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  import crypto from 'node:crypto';
@@ -55,11 +55,11 @@ export default class NestAccfactory {
55
55
 
56
56
  static DataSource = {
57
57
  REST: 'REST', // From the REST API
58
- PROTOBUF: 'PROTOBUF', // From the protobuf API
58
+ PROTOBUF: 'Protobuf', // From the Protobuf API
59
59
  };
60
60
 
61
- static GoogleConnection = 'google'; // Google account connection
62
- static NestConnection = 'nest'; // Nest account connection
61
+ static GoogleAccount = 'google'; // Google account connection
62
+ static NestAccount = 'nest'; // Nest account connection
63
63
 
64
64
  cachedAccessories = []; // Track restored cached accessories
65
65
 
@@ -67,6 +67,9 @@ export default class NestAccfactory {
67
67
  #connections = {}; // Object of confirmed connections
68
68
  #rawData = {}; // Cached copy of data from both Rest and Protobuf APIs
69
69
  #eventEmitter = new EventEmitter(); // Used for object messaging from this platform
70
+ #connectionTimer = undefined;
71
+ #protobufRoot = null; // Protobuf loaded protos
72
+ #trackedDevices = {}; // Object of devices we've created. used to track data source type, comms uuid. key'd by serial #
70
73
 
71
74
  constructor(log, config, api) {
72
75
  this.config = config;
@@ -80,7 +83,7 @@ export default class NestAccfactory {
80
83
  if (this.config[key]?.access_token !== undefined && this.config[key].access_token !== '') {
81
84
  // Nest account connection, assign a random UUID for each connection
82
85
  this.#connections[crypto.randomUUID()] = {
83
- type: NestAccfactory.NestConnection,
86
+ type: NestAccfactory.NestAccount,
84
87
  authorised: false,
85
88
  access_token: this.config[key].access_token,
86
89
  fieldTest: this.config[key]?.fieldTest === true,
@@ -98,7 +101,7 @@ export default class NestAccfactory {
98
101
  ) {
99
102
  // Google account connection, assign a random UUID for each connection
100
103
  this.#connections[crypto.randomUUID()] = {
101
- type: NestAccfactory.GoogleConnection,
104
+ type: NestAccfactory.GoogleAccount,
102
105
  authorised: false,
103
106
  issuetoken: this.config[key].issuetoken,
104
107
  cookie: this.config[key].cookie,
@@ -122,14 +125,14 @@ export default class NestAccfactory {
122
125
  }
123
126
 
124
127
  this.config.options.eveHistory = typeof this.config.options?.eveHistory === 'boolean' ? this.config.options.eveHistory : false;
125
- this.config.options.elevation = typeof this.config.options?.elevation === 'number' ? this.config.options.elevation : 0;
128
+ this.config.options.elevation = isNaN(this.config.options?.elevation) === false ? Number(this.config.options.elevation) : 0;
126
129
  this.config.options.weather = typeof this.config.options?.weather === 'boolean' ? this.config.options.weather : false;
127
130
  this.config.options.hksv = typeof this.config.options?.hksv === 'boolean' ? this.config.options.hksv : false;
128
131
 
129
132
  // Get configuration for max number of concurrent 'live view' streams. For HomeKit Secure Video, this will always be 1
130
133
  this.config.options.maxStreams =
131
- typeof this.config.options?.maxStreams === 'number' && this.deviceData?.hksv === false
132
- ? this.config.options.maxStreams
134
+ isNaN(this.config.options?.maxStreams) === false && this.deviceData?.hksv === false
135
+ ? Number(this.config.options.maxStreams)
133
136
  : this.deviceData?.hksv === true
134
137
  ? 1
135
138
  : 2;
@@ -137,30 +140,37 @@ export default class NestAccfactory {
137
140
  // Check if a ffmpeg binary exists in current path OR the specific path via configuration
138
141
  // If using HomeBridge, the default path will be where the Homebridge user folder is, otherwise the current directory
139
142
  this.config.options.ffmpeg = {};
140
- this.config.options.ffmpeg['path'] =
143
+ this.config.options.ffmpeg.binary = path.resolve(
141
144
  typeof this.config.options?.ffmpegPath === 'string' && this.config.options.ffmpegPath !== ''
142
145
  ? this.config.options.ffmpegPath
143
146
  : typeof api?.user?.storagePath === 'function'
144
147
  ? api.user.storagePath()
145
- : __dirname;
148
+ : __dirname,
149
+ );
150
+
151
+ // If the path doesn't include 'ffmpeg' on the end, we'll add it here
152
+ if (this.config.options.ffmpeg.binary.endsWith('/ffmpeg') === false) {
153
+ this.config.options.ffmpeg.binary = this.config.options.ffmpeg.binary + '/ffmpeg';
154
+ }
146
155
 
147
- this.config.options.ffmpeg['version'] = undefined;
156
+ this.config.options.ffmpeg.version = undefined;
148
157
  this.config.options.ffmpeg.libspeex = false;
158
+ this.config.options.ffmpeg.libopus = false;
149
159
  this.config.options.ffmpeg.libx264 = false;
150
160
  this.config.options.ffmpeg.libfdk_aac = false;
151
161
 
152
- if (fs.existsSync(path.resolve(this.config.options.ffmpeg.path + '/ffmpeg')) === false) {
162
+ if (fs.existsSync(this.config.options.ffmpeg.binary) === false) {
153
163
  if (this?.log?.warn) {
154
- this.log.warn('No ffmpeg binary found in "%s"', this.config.options.ffmpeg.path);
164
+ this.log.warn('Specified ffmpeg binary "%s" was not found', this.config.options.ffmpeg.binary);
155
165
  this.log.warn('Stream video/recording from camera/doorbells will be unavailable');
156
166
  }
157
167
 
158
168
  // If we flag ffmpegPath as undefined, no video streaming/record support enabled for camers/doorbells
159
- this.config.options.ffmpeg.path = undefined;
169
+ this.config.options.ffmpeg.binary = undefined;
160
170
  }
161
171
 
162
- if (fs.existsSync(path.resolve(this.config.options.ffmpeg.path + '/ffmpeg')) === true) {
163
- let ffmpegProcess = child_process.spawnSync(path.resolve(this.config.options.ffmpeg.path + '/ffmpeg'), ['-version'], {
172
+ if (fs.existsSync(this.config.options.ffmpeg.binary) === true) {
173
+ let ffmpegProcess = child_process.spawnSync(this.config.options.ffmpeg.binary, ['-version'], {
164
174
  env: process.env,
165
175
  });
166
176
  if (ffmpegProcess.stdout !== null) {
@@ -171,17 +181,18 @@ export default class NestAccfactory {
171
181
 
172
182
  // Determine what libraries ffmpeg is compiled with
173
183
  this.config.options.ffmpeg.libspeex = ffmpegProcess.stdout.toString().includes('--enable-libspeex') === true;
184
+ this.config.options.ffmpeg.libopus = ffmpegProcess.stdout.toString().includes('--enable-libopus') === true;
174
185
  this.config.options.ffmpeg.libx264 = ffmpegProcess.stdout.toString().includes('--enable-libx264') === true;
175
186
  this.config.options.ffmpeg.libfdk_aac = ffmpegProcess.stdout.toString().includes('--enable-libfdk-aac') === true;
176
-
177
187
  if (
178
188
  this.config.options.ffmpeg.version.replace(/\./gi, '') < parseFloat(FFMPEGVERSION.toString().replace(/\./gi, '')) ||
179
189
  this.config.options.ffmpeg.libspeex === false ||
190
+ this.config.options.ffmpeg.libopus === false ||
180
191
  this.config.options.ffmpeg.libx264 === false ||
181
192
  this.config.options.ffmpeg.libfdk_aac === false
182
193
  ) {
183
194
  this?.log?.warn &&
184
- this.log.warn('ffmpeg binary in "%s" does not meet the minimum support requirements', this.config.options.ffmpeg.path);
195
+ this.log.warn('ffmpeg binary "%s" does not meet the minimum support requirements', this.config.options.ffmpeg.binary);
185
196
  if (this.config.options.ffmpeg.version.replace(/\./gi, '') < parseFloat(FFMPEGVERSION.toString().replace(/\./gi, ''))) {
186
197
  this?.log?.warn &&
187
198
  this.log.warn(
@@ -191,22 +202,41 @@ export default class NestAccfactory {
191
202
  );
192
203
  this?.log?.warn && this.log.warn('Stream video/recording from camera/doorbells will be unavailable');
193
204
 
194
- this.config.options.ffmpeg.path = undefined; // No ffmpeg since below min version
205
+ this.config.options.ffmpeg.binary = undefined; // No ffmpeg since below min version
195
206
  }
196
207
  if (
197
208
  this.config.options.ffmpeg.libspeex === false &&
198
- (this.config.options.ffmpeg.libx264 === true && this.config.options.ffmpeg.libfdk_aac) === true
209
+ this.config.options.ffmpeg.libx264 === true &&
210
+ this.config.options.ffmpeg.libfdk_aac === true
199
211
  ) {
200
- this?.log?.warn && this.log.warn('Missing libspeex in ffmpeg binary, two-way audio on camera/doorbells will be unavailable');
212
+ this?.log?.warn && this.log.warn('Missing libspeex in ffmpeg binary, talkback on certain camera/doorbells will be unavailable');
213
+ }
214
+ if (
215
+ this.config.options.ffmpeg.libx264 === true &&
216
+ this.config.options.ffmpeg.libfdk_aac === false &&
217
+ this.config.options.ffmpeg.libopus === false
218
+ ) {
219
+ this?.log?.warn &&
220
+ this.log.warn('Missing libfdk_aac and libopus in ffmpeg binary, audio from camera/doorbells will be unavailable');
201
221
  }
202
222
  if (this.config.options.ffmpeg.libx264 === true && this.config.options.ffmpeg.libfdk_aac === false) {
203
223
  this?.log?.warn && this.log.warn('Missing libfdk_aac in ffmpeg binary, audio from camera/doorbells will be unavailable');
204
224
  }
225
+ if (
226
+ this.config.options.ffmpeg.libx264 === true &&
227
+ this.config.options.ffmpeg.libfdk_aac === true &&
228
+ this.config.options.ffmpeg.libopus === false
229
+ ) {
230
+ this?.log?.warn &&
231
+ this.log.warn(
232
+ 'Missing libopus in ffmpeg binary, audio (including talkback) from certain camera/doorbells will be unavailable',
233
+ );
234
+ }
205
235
  if (this.config.options.ffmpeg.libx264 === false) {
206
236
  this?.log?.warn &&
207
237
  this.log.warn('Missing libx264 in ffmpeg binary, stream video/recording from camera/doorbells will be unavailable');
208
238
 
209
- this.config.options.ffmpeg.path = undefined; // No ffmpeg since we do not have all the required libraries
239
+ this.config.options.ffmpeg.binary = undefined; // No ffmpeg since we do not have all the required libraries
210
240
  }
211
241
  }
212
242
  }
@@ -216,14 +246,43 @@ export default class NestAccfactory {
216
246
  this.api.on('didFinishLaunching', async () => {
217
247
  // We got notified that Homebridge has finished loading, so we are ready to process
218
248
  this.discoverDevices();
249
+
250
+ // We'll check connection status every 15 seconds. We'll also handle token expiry/refresh this way
251
+ clearInterval(this.#connectionTimer);
252
+ this.#connectionTimer = setInterval(this.discoverDevices.bind(this), 15000);
219
253
  });
220
254
 
221
255
  this.api.on('shutdown', async () => {
222
- // We got notified that Homebridge is shutting down. Perform cleanup??
223
- this.#eventEmitter.removeAllListeners(HomeKitDevice.SET);
224
- this.#eventEmitter.removeAllListeners(HomeKitDevice.GET);
256
+ // We got notified that Homebridge is shutting down
257
+ // Perform cleanup some internal cleaning up
258
+ this.#eventEmitter.removeAllListeners();
259
+ Object.values(this.#trackedDevices).forEach((value) => {
260
+ if (value?.timers !== undefined) {
261
+ Object.values(value?.timers).forEach((timers) => {
262
+ clearInterval(timers);
263
+ });
264
+ }
265
+ });
266
+ this.#trackedDevices = {};
267
+ clearInterval(this.#connectionTimer);
268
+ this.#connectionTimer = undefined;
269
+ this.#rawData = {};
270
+ this.#protobufRoot = null;
271
+ this.#eventEmitter = undefined;
225
272
  });
226
273
  }
274
+
275
+ // Setup event listeners for set/get calls from devices if not already done so
276
+ this.#eventEmitter.addListener(HomeKitDevice.SET, (uuid, values) => {
277
+ this.#set(uuid, values);
278
+ });
279
+ this.#eventEmitter.addListener(HomeKitDevice.GET, async (uuid, values) => {
280
+ let results = await this.#get(uuid, values);
281
+ // Send the results back to the device via a special event (only if still active)
282
+ if (this.#eventEmitter !== undefined) {
283
+ this.#eventEmitter.emit(HomeKitDevice.GET + '->' + uuid, results);
284
+ }
285
+ });
227
286
  }
228
287
 
229
288
  configureAccessory(accessory) {
@@ -235,24 +294,21 @@ export default class NestAccfactory {
235
294
  }
236
295
 
237
296
  async discoverDevices() {
238
- // Setup event listeners for set/get calls from devices
239
- this.#eventEmitter.addListener(HomeKitDevice.SET, (deviceUUID, values) => this.#set(deviceUUID, values));
240
- this.#eventEmitter.addListener(HomeKitDevice.GET, (deviceUUID, values) => this.#get(deviceUUID, values));
241
-
242
297
  Object.keys(this.#connections).forEach((uuid) => {
243
- this.#connect(uuid).then(() => {
244
- if (this.#connections[uuid].authorised === true) {
245
- this.#subscribeREST(uuid, true);
246
- this.#subscribeProtobuf(uuid);
247
- }
248
- });
298
+ if (this.#connections[uuid].authorised === false) {
299
+ this.#connect(uuid).then(() => {
300
+ if (this.#connections[uuid].authorised === true) {
301
+ this.#subscribeREST(uuid, true);
302
+ this.#subscribeProtobuf(uuid, true);
303
+ }
304
+ });
305
+ }
249
306
  });
250
307
  }
251
308
 
252
309
  async #connect(connectionUUID) {
253
310
  if (typeof this.#connections?.[connectionUUID] === 'object') {
254
- this.#connections[connectionUUID].authorised === false; // Mark connection as no-longer authorised
255
- if (this.#connections[connectionUUID].type === NestAccfactory.GoogleConnection) {
311
+ if (this.#connections[connectionUUID].type === NestAccfactory.GoogleAccount) {
256
312
  // Google cookie method as refresh token method no longer supported by Google since October 2022
257
313
  // Instructions from homebridge_nest or homebridge_nest_cam to obtain this
258
314
  this?.log?.info &&
@@ -308,7 +364,6 @@ export default class NestAccfactory {
308
364
  this.#connections[connectionUUID].userID = data.userid;
309
365
  this.#connections[connectionUUID].transport_url = data.urls.transport_url;
310
366
  this.#connections[connectionUUID].weather_url = data.urls.weather_url;
311
- this.#connections[connectionUUID].protobufRoot = null;
312
367
  this.#connections[connectionUUID].token = googleToken;
313
368
  this.#connections[connectionUUID].cameraAPI = {
314
369
  key: 'Authorization',
@@ -318,7 +373,7 @@ export default class NestAccfactory {
318
373
  };
319
374
 
320
375
  // Set timeout for token expiry refresh
321
- this.#connections[connectionUUID].timer = clearInterval(this.#connections[connectionUUID].timer);
376
+ clearTimeout(this.#connections[connectionUUID].timer);
322
377
  this.#connections[connectionUUID].timer = setTimeout(
323
378
  () => {
324
379
  this?.log?.info && this.log.info('Performing periodic token refresh for Google account');
@@ -334,11 +389,17 @@ export default class NestAccfactory {
334
389
  // eslint-disable-next-line no-unused-vars
335
390
  .catch((error) => {
336
391
  // The token we used to obtained a Nest session failed, so overall authorisation failed
392
+ this.#connections[connectionUUID].authorised = false;
393
+ this?.log?.debug &&
394
+ this.log.debug(
395
+ 'Failed to connect using credential details for connection uuid "%s". A periodic retry event will be triggered',
396
+ connectionUUID,
397
+ );
337
398
  this?.log?.error && this.log.error('Authorisation failed using Google account');
338
399
  });
339
400
  }
340
401
 
341
- if (this.#connections[connectionUUID].type === NestAccfactory.NestConnection) {
402
+ if (this.#connections[connectionUUID].type === NestAccfactory.NestAccount) {
342
403
  // Nest access token method. Get WEBSITE2 cookie for use with camera API calls if needed later
343
404
  this?.log?.info &&
344
405
  this.log.info(
@@ -381,7 +442,6 @@ export default class NestAccfactory {
381
442
  this.#connections[connectionUUID].userID = data.userid;
382
443
  this.#connections[connectionUUID].transport_url = data.urls.transport_url;
383
444
  this.#connections[connectionUUID].weather_url = data.urls.weather_url;
384
- this.#connections[connectionUUID].protobufRoot = null;
385
445
  this.#connections[connectionUUID].token = this.#connections[connectionUUID].access_token;
386
446
  this.#connections[connectionUUID].cameraAPI = {
387
447
  key: 'cookie',
@@ -390,7 +450,7 @@ export default class NestAccfactory {
390
450
  };
391
451
 
392
452
  // Set timeout for token expiry refresh
393
- this.#connections[connectionUUID].timer = clearInterval(this.#connections[connectionUUID].timer);
453
+ clearTimeout(this.#connections[connectionUUID].timer);
394
454
  this.#connections[connectionUUID].timer = setTimeout(
395
455
  () => {
396
456
  this?.log?.info && this.log.info('Performing periodic token refresh for Nest account');
@@ -405,6 +465,8 @@ export default class NestAccfactory {
405
465
  // eslint-disable-next-line no-unused-vars
406
466
  .catch((error) => {
407
467
  // The token we used to obtained a Nest session failed, so overall authorisation failed
468
+ this.#connections[connectionUUID].authorised = false;
469
+ this?.log?.debug && this.log.debug('Failed to connect using credential details for connection uuid "%s"', connectionUUID);
408
470
  this?.log?.error && this.log.error('Authorisation failed using Nest account');
409
471
  });
410
472
  }
@@ -433,13 +495,6 @@ export default class NestAccfactory {
433
495
  'widget_track',
434
496
  'quartz',
435
497
  ];
436
- const DEVICEBUCKETS = {
437
- structure: ['latitude', 'longitude'],
438
- device: ['where_id'],
439
- kryptonite: ['where_id', 'structure_id'],
440
- topaz: ['where_id', 'structure_id'],
441
- quartz: ['where_id', 'structure_id', 'nexus_api_http_server_url'],
442
- };
443
498
 
444
499
  // By default, setup for a full data read from the REST API
445
500
  let subscribeURL =
@@ -451,7 +506,7 @@ export default class NestAccfactory {
451
506
  let subscribeJSONData = { known_bucket_types: REQUIREDBUCKETS, known_bucket_versions: [] };
452
507
 
453
508
  if (fullRefresh === false) {
454
- // We have data stored from ths REST API, so setup read using known objects
509
+ // We have data stored from this REST API, so setup read using known object
455
510
  subscribeURL = this.#connections[connectionUUID].transport_url + '/v6/subscribe';
456
511
  subscribeJSONData = { objects: [] };
457
512
 
@@ -467,6 +522,10 @@ export default class NestAccfactory {
467
522
  });
468
523
  }
469
524
 
525
+ if (fullRefresh === true) {
526
+ this?.log?.debug && this.log.debug('Starting REST API subscribe for connection uuid "%s"', connectionUUID);
527
+ }
528
+
470
529
  fetchWrapper(
471
530
  'post',
472
531
  subscribeURL,
@@ -477,13 +536,12 @@ export default class NestAccfactory {
477
536
  Authorization: 'Basic ' + this.#connections[connectionUUID].token,
478
537
  },
479
538
  keepalive: true,
480
- timeout: 300 * 60,
539
+ //timeout: (5 * 60000),
481
540
  },
482
541
  JSON.stringify(subscribeJSONData),
483
542
  )
484
543
  .then((response) => response.json())
485
544
  .then(async (data) => {
486
- let deviceChanges = []; // No REST API devices changes to start with
487
545
  if (typeof data?.updated_buckets === 'object') {
488
546
  // This response is full data read
489
547
  data = data.updated_buckets;
@@ -555,7 +613,11 @@ export default class NestAccfactory {
555
613
  value.value.properties = data.items[0].properties;
556
614
  })
557
615
  .catch((error) => {
558
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
616
+ if (
617
+ error?.cause !== undefined &&
618
+ JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
619
+ this?.log?.debug
620
+ ) {
559
621
  this.log.debug('REST API had error retrieving camera/doorbell details during subscribe. Error was "%s"', error?.code);
560
622
  }
561
623
  });
@@ -591,7 +653,11 @@ export default class NestAccfactory {
591
653
  value.value.activity_zones = zones;
592
654
  })
593
655
  .catch((error) => {
594
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
656
+ if (
657
+ error?.cause !== undefined &&
658
+ JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
659
+ this?.log?.debug
660
+ ) {
595
661
  this.log.debug(
596
662
  'REST API had error retrieving camera/doorbell activity zones during subscribe. Error was "%s"',
597
663
  error?.code,
@@ -619,8 +685,30 @@ export default class NestAccfactory {
619
685
  // Object is present in the old buckets list, but not in the new buckets list
620
686
  // so we assume it has been removed
621
687
  // It also could mean device(s) have been removed from Nest
622
- if (Object.keys(DEVICEBUCKETS).includes(object_key.split('.')[0]) === true) {
623
- deviceChanges.push({ object_key: object_key, change: 'remove' });
688
+ if (
689
+ object_key.startsWith('structure.') === true ||
690
+ object_key.startsWith('device.') === true ||
691
+ object_key.startsWith('kryptonite.') === true ||
692
+ object_key.startsWith('topaz.') === true ||
693
+ object_key.startsWith('quartz.') === true
694
+ ) {
695
+ // Tidy up tracked devices since this one is removed
696
+ if (this.#trackedDevices[this.#rawData?.[object_key]?.value?.serial_number] !== undefined) {
697
+ // Remove any active running timers we have for this device
698
+ Object.values(this.#trackedDevices[this.#rawData[object_key].value.serial_number].timers).forEach((timers) => {
699
+ clearInterval(timers);
700
+ });
701
+
702
+ // Send removed notice onto HomeKit device for it to process
703
+ this.#eventEmitter.emit(
704
+ this.#trackedDevices[this.#rawData[object_key].value.serial_number].uuid,
705
+ HomeKitDevice.REMOVE,
706
+ {},
707
+ );
708
+
709
+ // Finally, remove from tracked devices
710
+ delete this.#trackedDevices[this.#rawData[object_key].value.serial_number];
711
+ }
624
712
  }
625
713
  delete this.#rawData[object_key];
626
714
  }
@@ -635,21 +723,9 @@ export default class NestAccfactory {
635
723
  this.#rawData[value.object_key].object_timestamp = value.object_timestamp;
636
724
  this.#rawData[value.object_key].connection = connectionUUID;
637
725
  this.#rawData[value.object_key].source = NestAccfactory.DataSource.REST;
638
- this.#rawData[value.object_key].timers = {}; // No timers running for this object
639
726
  this.#rawData[value.object_key].value = {};
640
727
  }
641
728
 
642
- // Need to check for a possible device addition to the raw REST API data.
643
- // We expect the devices we want to add, have certain minimum properties present in the data
644
- // We'll perform that check here
645
- if (
646
- Object.keys(DEVICEBUCKETS).includes(value.object_key.split('.')[0]) === true &&
647
- DEVICEBUCKETS[value.object_key.split('.')[0]].every((key) => key in value.value) === true &&
648
- DEVICEBUCKETS[value.object_key.split('.')[0]].every((key) => key in this.#rawData[value.object_key].value) === false
649
- ) {
650
- deviceChanges.push({ object_key: value.object_key, change: 'add' });
651
- }
652
-
653
729
  // Finally, update our internal raw REST API data with the new values
654
730
  this.#rawData[value.object_key].object_revision = value.object_revision; // Used for future subscribes
655
731
  this.#rawData[value.object_key].object_timestamp = value.object_timestamp; // Used for future subscribes
@@ -659,19 +735,21 @@ export default class NestAccfactory {
659
735
  }),
660
736
  );
661
737
 
662
- await this.#processPostSubscribe(deviceChanges);
738
+ await this.#processPostSubscribe();
663
739
  })
664
740
  .catch((error) => {
665
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
666
- this.log.debug('REST API had an error performing subscription. Will restart subscription');
741
+ if (error?.cause !== undefined && JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false && this?.log?.debug) {
742
+ this.log.debug('REST API had an error performing subscription with connection uuid "%s"', connectionUUID);
743
+ this.log.debug('Error was "%s"', error);
667
744
  }
668
745
  })
669
746
  .finally(() => {
747
+ this?.log?.debug && this.log.debug('Restarting REST API subscribe for connection uuid "%s"', connectionUUID);
670
748
  setTimeout(this.#subscribeREST.bind(this, connectionUUID, fullRefresh), 1000);
671
749
  });
672
750
  }
673
751
 
674
- async #subscribeProtobuf(connectionUUID) {
752
+ async #subscribeProtobuf(connectionUUID, firstRun) {
675
753
  if (typeof this.#connections?.[connectionUUID] !== 'object' || this.#connections?.[connectionUUID]?.authorised !== true) {
676
754
  // Not a valid connection object and/or we're not authorised
677
755
  return;
@@ -712,510 +790,535 @@ export default class NestAccfactory {
712
790
  }
713
791
  };
714
792
 
715
- // Attempt to load in protobuf files if not already done so for this connection
716
- if (
717
- this.#connections[connectionUUID].protobufRoot === null &&
718
- fs.existsSync(path.resolve(__dirname + '/protobuf/root.proto')) === true
719
- ) {
793
+ // Attempt to load in protobuf files if not already done so
794
+ if (this.#protobufRoot === null && fs.existsSync(path.resolve(__dirname + '/protobuf/root.proto')) === true) {
720
795
  protobuf.util.Long = null;
721
796
  protobuf.configure();
722
- this.#connections[connectionUUID].protobufRoot = protobuf.loadSync(path.resolve(__dirname + '/protobuf/root.proto'));
797
+ this.#protobufRoot = protobuf.loadSync(path.resolve(__dirname + '/protobuf/root.proto'));
798
+ if (this.#protobufRoot !== null) {
799
+ this?.log?.debug && this.log.debug('Loaded protobuf support files for Protobuf API');
800
+ }
723
801
  }
724
802
 
725
- if (this.#connections[connectionUUID].protobufRoot !== null) {
726
- // Loaded in the Protobuf files, so now dynamically build the 'observe' post body data based on what we have loaded
727
- let observeTraitsList = [];
728
- let observeBody = Buffer.alloc(0);
729
- let traitTypeObserveParam = this.#connections[connectionUUID].protobufRoot.lookup('nestlabs.gateway.v2.TraitTypeObserveParams');
730
- let observeRequest = this.#connections[connectionUUID].protobufRoot.lookup('nestlabs.gateway.v2.ObserveRequest');
731
- if (traitTypeObserveParam !== null && observeRequest !== null) {
732
- traverseTypes(this.#connections[connectionUUID].protobufRoot, (type) => {
733
- // We only want to have certain trait'families' in our observe reponse we are building
734
- // This also depends on the account type we connected with
735
- // Nest accounts cannot observe camera/doorbell product traits
736
- if (
737
- (this.#connections[connectionUUID].type === NestAccfactory.NestConnection &&
738
- type.fullName.startsWith('.nest.trait.product.camera') === false &&
739
- type.fullName.startsWith('.nest.trait.product.doorbell') === false &&
740
- (type.fullName.startsWith('.nest.trait') === true || type.fullName.startsWith('.weave.') === true)) ||
741
- (this.#connections[connectionUUID].type === NestAccfactory.GoogleConnection &&
742
- (type.fullName.startsWith('.nest.trait') === true ||
743
- type.fullName.startsWith('.weave.') === true ||
744
- type.fullName.startsWith('.google.trait.product.camera') === true))
745
- ) {
746
- observeTraitsList.push(traitTypeObserveParam.create({ traitType: type.fullName.replace(/^\.*|\.*$/g, '') }));
747
- }
748
- });
749
- observeBody = observeRequest.encode(observeRequest.create({ stateTypes: [1, 2], traitTypeParams: observeTraitsList })).finish();
750
- }
803
+ if (this.#protobufRoot === null) {
804
+ this?.log?.warn &&
805
+ this.log.warn('Failed to loaded Protobuf API support files. This will cause certain Nest/Google devices to be un-supported');
806
+ return;
807
+ }
751
808
 
752
- fetchWrapper(
753
- 'post',
754
- 'https://' + this.#connections[connectionUUID].protobufAPIHost + '/nestlabs.gateway.v2.GatewayService/Observe',
755
- {
756
- headers: {
757
- referer: 'https://' + this.#connections[connectionUUID].referer,
758
- 'User-Agent': USERAGENT,
759
- Authorization: 'Basic ' + this.#connections[connectionUUID].token,
760
- 'Content-Type': 'application/x-protobuf',
761
- 'X-Accept-Content-Transfer-Encoding': 'binary',
762
- 'X-Accept-Response-Streaming': 'true',
763
- },
764
- keepalive: true,
765
- timeout: 300 * 60,
809
+ // We have loaded Protobuf proto files, so now dynamically build the 'observe' post body data
810
+ let observeTraitsList = [];
811
+ let observeBody = Buffer.alloc(0);
812
+ let traitTypeObserveParam = this.#protobufRoot.lookup('nestlabs.gateway.v2.TraitTypeObserveParams');
813
+ let observeRequest = this.#protobufRoot.lookup('nestlabs.gateway.v2.ObserveRequest');
814
+ if (traitTypeObserveParam !== null && observeRequest !== null) {
815
+ traverseTypes(this.#protobufRoot, (type) => {
816
+ // We only want to have certain trait'families' in our observe reponse we are building
817
+ // This also depends on the account type we connected with
818
+ // Nest accounts cannot observe camera/doorbell product traits
819
+ if (
820
+ (this.#connections[connectionUUID].type === NestAccfactory.NestAccount &&
821
+ type.fullName.startsWith('.nest.trait.product.camera') === false &&
822
+ type.fullName.startsWith('.nest.trait.product.doorbell') === false &&
823
+ (type.fullName.startsWith('.nest.trait') === true || type.fullName.startsWith('.weave.') === true)) ||
824
+ (this.#connections[connectionUUID].type === NestAccfactory.GoogleAccount &&
825
+ (type.fullName.startsWith('.nest.trait') === true ||
826
+ type.fullName.startsWith('.weave.') === true ||
827
+ type.fullName.startsWith('.google.trait.product.camera') === true))
828
+ ) {
829
+ observeTraitsList.push(traitTypeObserveParam.create({ traitType: type.fullName.replace(/^\.*|\.*$/g, '') }));
830
+ }
831
+ });
832
+ observeBody = observeRequest.encode(observeRequest.create({ stateTypes: [1, 2], traitTypeParams: observeTraitsList })).finish();
833
+ }
834
+
835
+ if (firstRun === true) {
836
+ this?.log?.debug && this.log.debug('Starting Protobuf API trait observe for connection uuid "%s"', connectionUUID);
837
+ }
838
+
839
+ fetchWrapper(
840
+ 'post',
841
+ 'https://' + this.#connections[connectionUUID].protobufAPIHost + '/nestlabs.gateway.v2.GatewayService/Observe',
842
+ {
843
+ headers: {
844
+ referer: 'https://' + this.#connections[connectionUUID].referer,
845
+ 'User-Agent': USERAGENT,
846
+ Authorization: 'Basic ' + this.#connections[connectionUUID].token,
847
+ 'Content-Type': 'application/x-protobuf',
848
+ 'X-Accept-Content-Transfer-Encoding': 'binary',
849
+ 'X-Accept-Response-Streaming': 'true',
766
850
  },
767
- observeBody,
768
- )
769
- .then((response) => response.body)
770
- .then(async (data) => {
771
- let deviceChanges = []; // No Protobuf API devices changes to start with
772
- let buffer = Buffer.alloc(0);
773
- for await (const chunk of data) {
774
- buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
775
- let messageSize = calculate_message_size(buffer);
776
- if (buffer.length >= messageSize) {
777
- let decodedMessage = {};
778
- try {
779
- // Attempt to decode the Protobuf message(s) we extracted from the stream and get a JSON object representation
780
- decodedMessage = this.#connections[connectionUUID].protobufRoot
781
- .lookup('nestlabs.gateway.v2.ObserveResponse')
782
- .decode(buffer.subarray(0, messageSize))
783
- .toJSON();
784
-
785
- // Tidy up our received messages. This ensures we only have one status for the trait in the data we process
786
- // We'll favour a trait with accepted status over the same with confirmed status
787
- if (decodedMessage?.observeResponse?.[0]?.traitStates !== undefined) {
788
- let notAcceptedStatus = decodedMessage.observeResponse[0].traitStates.filter(
789
- (trait) => trait.stateTypes.includes('ACCEPTED') === false,
790
- );
791
- let acceptedStatus = decodedMessage.observeResponse[0].traitStates.filter(
792
- (trait) => trait.stateTypes.includes('ACCEPTED') === true,
793
- );
794
- let difference = acceptedStatus.map((trait) => trait.traitId.resourceId + '/' + trait.traitId.traitLabel);
795
- decodedMessage.observeResponse[0].traitStates =
796
- ((notAcceptedStatus = notAcceptedStatus.filter(
797
- (trait) => difference.includes(trait.traitId.resourceId + '/' + trait.traitId.traitLabel) === false,
798
- )),
799
- [...notAcceptedStatus, ...acceptedStatus]);
800
- }
801
- // We'll use the resource status message to look for structure and/or device removals
802
- // We could also check for structure and/or device additions here, but we'll want to be flagged
803
- // that a device is 'ready' for use before we add in. This data is populated in the trait data
804
- if (decodedMessage?.observeResponse?.[0]?.resourceMetas !== undefined) {
805
- decodedMessage.observeResponse[0].resourceMetas.map(async (resource) => {
806
- if (
807
- resource.status === 'REMOVED' &&
808
- (resource.resourceId.startsWith('STRUCTURE_') || resource.resourceId.startsWith('DEVICE_'))
809
- ) {
810
- // We have the removal of a 'home' and/ device
811
- deviceChanges.push({ object_key: resource.resourceId, change: 'removed' });
812
- }
813
- });
814
- }
815
- // eslint-disable-next-line no-unused-vars
816
- } catch (error) {
817
- // Empty
851
+ keepalive: true,
852
+ //timeout: (5 * 60000),
853
+ },
854
+ observeBody,
855
+ )
856
+ .then((response) => response.body)
857
+ .then(async (data) => {
858
+ let buffer = Buffer.alloc(0);
859
+ for await (const chunk of data) {
860
+ buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
861
+ let messageSize = calculate_message_size(buffer);
862
+ if (buffer.length >= messageSize) {
863
+ let decodedMessage = {};
864
+ try {
865
+ // Attempt to decode the Protobuf message(s) we extracted from the stream and get a JSON object representation
866
+ decodedMessage = this.#protobufRoot
867
+ .lookup('nestlabs.gateway.v2.ObserveResponse')
868
+ .decode(buffer.subarray(0, messageSize))
869
+ .toJSON();
870
+
871
+ // Tidy up our received messages. This ensures we only have one status for the trait in the data we process
872
+ // We'll favour a trait with accepted status over the same with confirmed status
873
+ if (decodedMessage?.observeResponse?.[0]?.traitStates !== undefined) {
874
+ let notAcceptedStatus = decodedMessage.observeResponse[0].traitStates.filter(
875
+ (trait) => trait.stateTypes.includes('ACCEPTED') === false,
876
+ );
877
+ let acceptedStatus = decodedMessage.observeResponse[0].traitStates.filter(
878
+ (trait) => trait.stateTypes.includes('ACCEPTED') === true,
879
+ );
880
+ let difference = acceptedStatus.map((trait) => trait.traitId.resourceId + '/' + trait.traitId.traitLabel);
881
+ decodedMessage.observeResponse[0].traitStates =
882
+ ((notAcceptedStatus = notAcceptedStatus.filter(
883
+ (trait) => difference.includes(trait.traitId.resourceId + '/' + trait.traitId.traitLabel) === false,
884
+ )),
885
+ [...notAcceptedStatus, ...acceptedStatus]);
818
886
  }
819
- buffer = buffer.subarray(messageSize); // Remove the message from the beginning of the buffer
887
+ // We'll use the resource status message to look for structure and/or device removals
888
+ // We could also check for structure and/or device additions here, but we'll want to be flagged
889
+ // that a device is 'ready' for use before we add in. This data is populated in the trait data
890
+ if (decodedMessage?.observeResponse?.[0]?.resourceMetas !== undefined) {
891
+ decodedMessage.observeResponse[0].resourceMetas.map(async (resource) => {
892
+ if (
893
+ resource.status === 'REMOVED' &&
894
+ (resource.resourceId.startsWith('STRUCTURE_') || resource.resourceId.startsWith('DEVICE_'))
895
+ ) {
896
+ // We have the removal of a 'home' and/or device
897
+ // Tidy up tracked devices since this one is removed
898
+ if (this.#trackedDevices[this.#rawData?.[resource.resourceId]?.value?.device_identity?.serialNumber] !== undefined) {
899
+ // Remove any active running timers we have for this device
900
+ Object.values(
901
+ this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber]?.timers,
902
+ ).forEach((timers) => {
903
+ clearInterval(timers);
904
+ });
820
905
 
821
- if (typeof decodedMessage?.observeResponse?.[0]?.traitStates === 'object') {
822
- await Promise.all(
823
- decodedMessage.observeResponse[0].traitStates.map(async (trait) => {
824
- if (trait.traitId.traitLabel === 'configuration_done') {
825
- if (
826
- this.#rawData[trait.traitId.resourceId]?.value?.configuration_done?.deviceReady !== true &&
827
- trait.patch.values?.deviceReady === true
828
- ) {
829
- deviceChanges.push({ object_key: trait.traitId.resourceId, change: 'add' });
830
- }
831
- }
832
- if (trait.traitId.traitLabel === 'camera_migration_status') {
833
- // Handle case of camera/doorbell(s) which have been migrated from Nest to Google Home
834
- if (
835
- this.#rawData[trait.traitId.resourceId]?.value?.camera_migration_status?.state?.where !==
836
- 'MIGRATED_TO_GOOGLE_HOME' &&
837
- trait.patch.values?.state?.where === 'MIGRATED_TO_GOOGLE_HOME' &&
838
- this.#rawData[trait.traitId.resourceId]?.value?.camera_migration_status?.state?.progress !== 'PROGRESS_COMPLETE' &&
839
- trait.patch.values?.state?.progress === 'PROGRESS_COMPLETE'
840
- ) {
841
- deviceChanges.push({ object_key: trait.traitId.resourceId, change: 'add' });
842
- }
843
- }
906
+ // Send removed notice onto HomeKit device for it to process
907
+ this.#eventEmitter.emit(
908
+ this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber].uuid,
909
+ HomeKitDevice.REMOVE,
910
+ {},
911
+ );
844
912
 
845
- if (typeof this.#rawData[trait.traitId.resourceId] === 'undefined') {
846
- this.#rawData[trait.traitId.resourceId] = {};
847
- this.#rawData[trait.traitId.resourceId].connection = connectionUUID;
848
- this.#rawData[trait.traitId.resourceId].source = NestAccfactory.DataSource.PROTOBUF;
849
- this.#rawData[trait.traitId.resourceId].timers = {}; // No timers running for this object
850
- this.#rawData[trait.traitId.resourceId].value = {};
913
+ // Finally, remove from tracked devices
914
+ delete this.#trackedDevices[this.#rawData?.[resource.resourceId].value.device_identity.serialNumber];
851
915
  }
852
- this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel] =
853
- typeof trait.patch.values !== 'undefined' ? trait.patch.values : {};
916
+ delete this.#rawData[resource.resourceId];
917
+ }
918
+ });
919
+ }
920
+ // eslint-disable-next-line no-unused-vars
921
+ } catch (error) {
922
+ // Empty
923
+ }
924
+ buffer = buffer.subarray(messageSize); // Remove the message from the beginning of the buffer
925
+
926
+ if (typeof decodedMessage?.observeResponse?.[0]?.traitStates === 'object') {
927
+ await Promise.all(
928
+ decodedMessage.observeResponse[0].traitStates.map(async (trait) => {
929
+ if (typeof this.#rawData[trait.traitId.resourceId] === 'undefined') {
930
+ this.#rawData[trait.traitId.resourceId] = {};
931
+ this.#rawData[trait.traitId.resourceId].connection = connectionUUID;
932
+ this.#rawData[trait.traitId.resourceId].source = NestAccfactory.DataSource.PROTOBUF;
933
+ this.#rawData[trait.traitId.resourceId].value = {};
934
+ }
935
+ this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel] =
936
+ typeof trait.patch.values !== 'undefined' ? trait.patch.values : {};
854
937
 
855
- // We don't need to store the trait type, so remove it
856
- delete this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel]['@type'];
938
+ // We don't need to store the trait type, so remove it
939
+ delete this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel]['@type'];
857
940
 
858
- // If we have structure location details and associated geo-location details, get the weather data for the location
859
- // We'll store this in the object key/value as per REST API
860
- if (
861
- trait.traitId.resourceId.startsWith('STRUCTURE_') === true &&
862
- trait.traitId.traitLabel === 'structure_location' &&
863
- typeof trait.patch.values?.geoCoordinate?.latitude === 'number' &&
864
- typeof trait.patch.values?.geoCoordinate?.longitude === 'number'
865
- ) {
866
- this.#rawData[trait.traitId.resourceId].value.weather = await this.#getWeatherData(
867
- connectionUUID,
868
- trait.traitId.resourceId,
869
- trait.patch.values.geoCoordinate.latitude,
870
- trait.patch.values.geoCoordinate.longitude,
871
- );
872
- }
873
- }),
874
- );
941
+ // If we have structure location details and associated geo-location details, get the weather data for the location
942
+ // We'll store this in the object key/value as per REST API
943
+ if (
944
+ trait.traitId.resourceId.startsWith('STRUCTURE_') === true &&
945
+ trait.traitId.traitLabel === 'structure_location' &&
946
+ isNaN(trait.patch.values?.geoCoordinate?.latitude) === false &&
947
+ isNaN(trait.patch.values?.geoCoordinate?.longitude) === false
948
+ ) {
949
+ this.#rawData[trait.traitId.resourceId].value.weather = await this.#getWeatherData(
950
+ connectionUUID,
951
+ trait.traitId.resourceId,
952
+ Number(trait.patch.values.geoCoordinate.latitude),
953
+ Number(trait.patch.values.geoCoordinate.longitude),
954
+ );
955
+ }
956
+ }),
957
+ );
875
958
 
876
- await this.#processPostSubscribe(deviceChanges);
877
- deviceChanges = []; // No more device changes now
878
- }
959
+ await this.#processPostSubscribe();
879
960
  }
880
961
  }
881
- })
882
- .catch((error) => {
883
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
884
- this.log.debug('Protobuf API had an error performing trait observe. Will restart observe');
885
- }
886
- })
887
- .finally(() => {
888
- setTimeout(this.#subscribeProtobuf.bind(this, connectionUUID), 1000);
889
- });
890
- }
962
+ }
963
+ })
964
+ .catch((error) => {
965
+ if (error?.cause !== undefined && JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false && this?.log?.debug) {
966
+ this.log.debug('Protobuf API had an error performing trait observe with connection uuid "%s"', connectionUUID);
967
+ this.log.debug('Error was "%s"', error);
968
+ }
969
+ })
970
+ .finally(() => {
971
+ this?.log?.debug && this.log.debug('Restarting Protobuf API trait observe for connection uuid "%s"', connectionUUID);
972
+ setTimeout(this.#subscribeProtobuf.bind(this, connectionUUID, false), 1000);
973
+ });
891
974
  }
892
975
 
893
- async #processPostSubscribe(deviceChanges) {
894
- // Process any device removals we have
895
- Object.values(deviceChanges)
896
- .filter((object) => object.change === 'remove')
897
- .forEach((object) => {
898
- if (typeof this.#rawData[object.object_key] === 'object') {
899
- // Remove any timers that might have been associated with this device
900
- Object.values(this.#rawData[object.object_key].timers).forEach((timerObject) => {
901
- clearInterval(timerObject);
902
- });
976
+ async #processPostSubscribe() {
977
+ Object.values(this.#processData('')).forEach((deviceData) => {
978
+ if (this.#trackedDevices?.[deviceData?.serialNumber] === undefined && deviceData?.excluded === true) {
979
+ // We haven't tracked this device before (ie: should be a new one) and but its excluded
980
+ this?.log?.warn && this.log.warn('Device "%s" is ignored due to it being marked as excluded', deviceData.description);
981
+ }
982
+ if (this.#trackedDevices?.[deviceData?.serialNumber] === undefined && deviceData?.excluded === false) {
983
+ // We haven't tracked this device before (ie: should be a new one) and its not excluded
984
+ // so create the required HomeKit accessories based upon the device data
985
+ if (deviceData.device_type === NestAccfactory.DeviceType.THERMOSTAT && typeof NestThermostat === 'function') {
986
+ // Nest Thermostat(s) - Categories.THERMOSTAT = 9
987
+ let tempDevice = new NestThermostat(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
988
+ tempDevice.add('Nest Thermostat', 9, true);
989
+ // Track this device once created
990
+ this.#trackedDevices[deviceData.serialNumber] = {
991
+ uuid: tempDevice.uuid,
992
+ rawDataUuid: deviceData.nest_google_uuid,
993
+ source: undefined,
994
+ };
995
+ }
903
996
 
904
- // Clean up structure
905
- delete this.#rawData[object.object_key];
997
+ if (deviceData.device_type === NestAccfactory.DeviceType.TEMPSENSOR && typeof NestTemperatureSensor === 'function') {
998
+ // Nest Temperature Sensor - Categories.SENSOR = 10;
999
+ let tempDevice = new NestTemperatureSensor(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
1000
+ tempDevice.add('Nest Temperature Sensor', 10, true);
1001
+ // Track this device once created
1002
+ this.#trackedDevices[deviceData.serialNumber] = {
1003
+ uuid: tempDevice.uuid,
1004
+ rawDataUuid: deviceData.nest_google_uuid,
1005
+ source: undefined,
1006
+ };
906
1007
  }
907
1008
 
908
- // Send removed notice onto HomeKit device for it to process
909
- // This allows handling removal of a device without knowing its previous data
910
- this.#eventEmitter.emit(object.object_key, HomeKitDevice.REMOVE, {});
911
- });
1009
+ if (deviceData.device_type === NestAccfactory.DeviceType.SMOKESENSOR && typeof NestProtect === 'function') {
1010
+ // Nest Protect(s) - Categories.SENSOR = 10
1011
+ let tempDevice = new NestProtect(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
1012
+ tempDevice.add('Nest Protect', 10, true);
1013
+ // Track this device once created
1014
+ this.#trackedDevices[deviceData.serialNumber] = {
1015
+ uuid: tempDevice.uuid,
1016
+ rawDataUuid: deviceData.nest_google_uuid,
1017
+ source: undefined,
1018
+ };
1019
+ }
912
1020
 
913
- Object.values(this.#processData('')).forEach((deviceData) => {
914
- // Process any device additions we have
915
- Object.values(deviceChanges)
916
- .filter((object) => object.change === 'add')
917
- .forEach((object) => {
918
- if (object.object_key === deviceData.uuid && deviceData.excluded === true) {
919
- this?.log?.warn && this.log.warn('Device "%s" ignored due to it being marked as excluded', deviceData.description);
1021
+ if (
1022
+ (deviceData.device_type === NestAccfactory.DeviceType.CAMERA ||
1023
+ deviceData.device_type === NestAccfactory.DeviceType.DOORBELL ||
1024
+ deviceData.device_type === NestAccfactory.DeviceType.FLOODLIGHT) &&
1025
+ (typeof NestCamera === 'function' || typeof NestDoorbell === 'function' || typeof NestFloodlight === 'function')
1026
+ ) {
1027
+ let accessoryName = 'Nest ' + deviceData.model.replace(/\s*(?:\([^()]*\))/gi, '');
1028
+ let tempDevice = undefined;
1029
+ if (deviceData.device_type === NestAccfactory.DeviceType.CAMERA) {
1030
+ // Nest Camera(s) - Categories.IP_CAMERA = 17
1031
+ tempDevice = new NestCamera(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
1032
+ tempDevice.add(accessoryName, 17, true);
1033
+ }
1034
+ if (deviceData.device_type === NestAccfactory.DeviceType.DOORBELL) {
1035
+ // Nest Doorbell(s) - Categories.VIDEO_DOORBELL = 18
1036
+ tempDevice = new NestDoorbell(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
1037
+ tempDevice.add(accessoryName, 18, true);
1038
+ }
1039
+ if (deviceData.device_type === NestAccfactory.DeviceType.FLOODLIGHT) {
1040
+ // Nest Camera(s) with Floodlight - Categories.IP_CAMERA = 17
1041
+ tempDevice = new NestFloodlight(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
1042
+ tempDevice.add(accessoryName, 17, true);
920
1043
  }
921
- if (object.object_key === deviceData.uuid && deviceData.excluded === false) {
922
- // Device isn't marked as excluded, so create the required HomeKit accessories based upon the device data
923
- if (deviceData.device_type === NestAccfactory.DeviceType.THERMOSTAT && typeof NestThermostat === 'function') {
924
- // Nest Thermostat(s) - Categories.THERMOSTAT = 9
925
- this?.log?.debug &&
926
- this.log.debug('Using "%s" data source for "%s"', this.#rawData[object.object_key]?.source, deviceData.description);
927
- let tempDevice = new NestThermostat(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
928
- tempDevice.add('Nest Thermostat', 9, true);
929
- }
930
-
931
- if (deviceData.device_type === NestAccfactory.DeviceType.TEMPSENSOR && typeof NestTemperatureSensor === 'function') {
932
- // Nest Temperature Sensor - Categories.SENSOR = 10
933
- this?.log?.debug &&
934
- this.log.debug('Using "%s" data source for "%s"', this.#rawData[object.object_key]?.source, deviceData.description);
935
- let tempDevice = new NestTemperatureSensor(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
936
- tempDevice.add('Nest Temperature Sensor', 10, true);
937
- }
938
1044
 
939
- if (deviceData.device_type === NestAccfactory.DeviceType.SMOKESENSOR && typeof NestProtect === 'function') {
940
- // Nest Protect(s) - Categories.SENSOR = 10
941
- this?.log?.debug &&
942
- this.log.debug('Using "%s" data source for "%s"', this.#rawData[object.object_key]?.source, deviceData.description);
943
- let tempDevice = new NestProtect(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
944
- tempDevice.add('Nest Protect', 10, true);
945
- }
1045
+ // Track this device once created
1046
+ this.#trackedDevices[deviceData.serialNumber] = {
1047
+ uuid: tempDevice.uuid,
1048
+ rawDataUuid: deviceData.nest_google_uuid,
1049
+ source: undefined,
1050
+ timers: {},
1051
+ };
946
1052
 
1053
+ // Setup polling loop for camera/doorbell zone data
1054
+ // This is only required for REST API data sources as these details are present in Protobuf API
1055
+ this.#trackedDevices[deviceData.serialNumber].timers.zones = setInterval(async () => {
1056
+ let nest_google_uuid = this.#trackedDevices?.[deviceData?.serialNumber]?.rawDataUuid;
947
1057
  if (
948
- (deviceData.device_type === NestAccfactory.DeviceType.CAMERA ||
949
- deviceData.device_type === NestAccfactory.DeviceType.DOORBELL ||
950
- deviceData.device_type === NestAccfactory.DeviceType.FLOODLIGHT) &&
951
- (typeof NestCamera === 'function' || typeof NestDoorbell === 'function' || typeof NestFloodlight === 'function')
1058
+ this.#rawData?.[nest_google_uuid]?.value !== undefined &&
1059
+ this.#trackedDevices?.[deviceData?.serialNumber]?.source === NestAccfactory.DataSource.REST
952
1060
  ) {
953
- this?.log?.debug &&
954
- this.log.debug('Using "%s" data source for "%s"', this.#rawData[object.object_key]?.source, deviceData.description);
955
-
956
- let accessoryName = 'Nest ' + deviceData.model.replace(/\s*(?:\([^()]*\))/gi, '');
957
- if (deviceData.device_type === NestAccfactory.DeviceType.CAMERA) {
958
- // Nest Camera(s) - Categories.IP_CAMERA = 17
959
- let tempDevice = new NestCamera(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
960
- tempDevice.add(accessoryName, 17, true);
961
- }
962
- if (deviceData.device_type === NestAccfactory.DeviceType.DOORBELL) {
963
- // Nest Doorbell(s) - Categories.VIDEO_DOORBELL = 18
964
- let tempDevice = new NestDoorbell(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
965
- tempDevice.add(accessoryName, 18, true);
966
- }
967
- if (deviceData.device_type === NestAccfactory.DeviceType.FLOODLIGHT) {
968
- // Nest Camera(s) with Floodlight - Categories.IP_CAMERA = 17
969
- let tempDevice = new NestFloodlight(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
970
- tempDevice.add(accessoryName, 17, true);
971
- }
972
-
973
- // Setup polling loop for camera/doorbell zone data if not already created.
974
- // This is only required for REST API data sources as these details are present in Protobuf API
975
- if (
976
- this.#rawData?.[object.object_key] !== undefined &&
977
- this.#rawData[object.object_key]?.timers?.zones === undefined &&
978
- this.#rawData[object.object_key].source === NestAccfactory.DataSource.REST
979
- ) {
980
- this.#rawData[object.object_key].timers.zones = setInterval(async () => {
981
- if (this.#rawData?.[object.object_key]?.value?.nexus_api_http_server_url !== undefined) {
982
- await fetchWrapper(
983
- 'get',
984
- this.#rawData[object.object_key].value.nexus_api_http_server_url +
985
- '/cuepoint_category/' +
986
- object.object_key.split('.')[1],
987
- {
988
- headers: {
989
- referer: 'https://' + this.#connections[this.#rawData[object.object_key].connection].referer,
990
- 'User-Agent': USERAGENT,
991
- [this.#connections[this.#rawData[object.object_key].connection].cameraAPI.key]:
992
- this.#connections[this.#rawData[object.object_key].connection].cameraAPI.value +
993
- this.#connections[this.#rawData[object.object_key].connection].cameraAPI.token,
994
- },
995
- timeout: CAMERAZONEPOLLING,
996
- },
997
- )
998
- .then((response) => response.json())
999
- .then((data) => {
1000
- let zones = [];
1001
- data.forEach((zone) => {
1002
- if (zone.type.toUpperCase() === 'ACTIVITY' || zone.type.toUpperCase() === 'REGION') {
1003
- zones.push({
1004
- id: zone.id === 0 ? 1 : zone.id,
1005
- name: makeHomeKitName(zone.label),
1006
- hidden: zone.hidden === true,
1007
- uri: zone.nexusapi_image_uri,
1008
- });
1009
- }
1010
- });
1061
+ await fetchWrapper(
1062
+ 'get',
1063
+ this.#rawData[nest_google_uuid].value.nexus_api_http_server_url + '/cuepoint_category/' + nest_google_uuid.split('.')[1],
1064
+ {
1065
+ headers: {
1066
+ referer: 'https://' + this.#connections[this.#rawData[nest_google_uuid].connection].referer,
1067
+ 'User-Agent': USERAGENT,
1068
+ [this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.key]:
1069
+ this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.value +
1070
+ this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.token,
1071
+ },
1072
+ timeout: CAMERAZONEPOLLING,
1073
+ },
1074
+ )
1075
+ .then((response) => response.json())
1076
+ .then((data) => {
1077
+ let zones = [];
1078
+ data.forEach((zone) => {
1079
+ if (zone.type.toUpperCase() === 'ACTIVITY' || zone.type.toUpperCase() === 'REGION') {
1080
+ zones.push({
1081
+ id: zone.id === 0 ? 1 : zone.id,
1082
+ name: makeHomeKitName(zone.label),
1083
+ hidden: zone.hidden === true,
1084
+ uri: zone.nexusapi_image_uri,
1085
+ });
1086
+ }
1087
+ });
1011
1088
 
1012
- this.#rawData[object.object_key].value.activity_zones = zones;
1089
+ // Update internal structure with new zone details.
1090
+ // We do a test to see if its still present not interval loop not finished or device removed
1091
+ if (this.#rawData?.[nest_google_uuid]?.value !== undefined) {
1092
+ this.#rawData[nest_google_uuid].value.activity_zones = zones;
1013
1093
 
1014
- // Send updated data onto HomeKit device for it to process
1015
- this.#eventEmitter.emit(object.object_key, HomeKitDevice.UPDATE, {
1016
- activity_zones: this.#rawData[object.object_key].value.activity_zones,
1017
- });
1018
- })
1019
- .catch((error) => {
1020
- // Log debug message if wasn't a timeout
1021
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
1022
- this.log.debug(
1023
- 'REST API had error retrieving camera/doorbell activity zones for uuid "%s". Error was "%s"',
1024
- object.object_key,
1025
- error?.code,
1026
- );
1027
- }
1094
+ // Send updated data onto HomeKit device for it to process
1095
+ if (this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
1096
+ this.#eventEmitter.emit(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, {
1097
+ activity_zones: zones,
1028
1098
  });
1099
+ }
1029
1100
  }
1030
- }, CAMERAZONEPOLLING);
1031
- }
1032
-
1033
- // Setup polling loop for camera/doorbell alert data if not already created
1034
- if (this.#rawData?.[object.object_key] !== undefined && this.#rawData?.[object.object_key]?.timers?.alerts === undefined) {
1035
- this.#rawData[object.object_key].timers.alerts = setInterval(async () => {
1101
+ })
1102
+ .catch((error) => {
1103
+ // Log debug message if wasn't a timeout
1036
1104
  if (
1037
- typeof this.#rawData[object.object_key]?.value === 'object' &&
1038
- this.#rawData[object.object_key]?.source === NestAccfactory.DataSource.PROTOBUF
1105
+ error?.cause !== undefined &&
1106
+ JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
1107
+ this?.log?.debug
1039
1108
  ) {
1040
- let alerts = []; // No alerts yet
1041
-
1042
- let commandResponse = await this.#protobufCommand(
1043
- this.#rawData[object.object_key].connection,
1044
- 'ResourceApi',
1045
- 'SendCommand',
1046
- {
1047
- resourceRequest: {
1048
- resourceId: object.object_key,
1049
- requestId: crypto.randomUUID(),
1050
- },
1051
- resourceCommands: [
1052
- {
1053
- traitLabel: 'camera_observation_history',
1054
- command: {
1055
- type_url:
1056
- 'type.nestlabs.com/nest.trait.history.CameraObservationHistoryTrait.CameraObservationHistoryRequest',
1057
- value: {
1058
- // We want camera history from now for upto 30secs from now
1059
- queryStartTime: { seconds: Math.floor(Date.now() / 1000), nanos: (Math.round(Date.now()) % 1000) * 1e6 },
1060
- queryEndTime: {
1061
- seconds: Math.floor((Date.now() + 30000) / 1000),
1062
- nanos: (Math.round(Date.now() + 30000) % 1000) * 1e6,
1063
- },
1064
- },
1065
- },
1066
- },
1067
- ],
1068
- },
1109
+ this.log.debug(
1110
+ 'REST API had error retrieving camera/doorbell activity zones for "%s". Error was "%s"',
1111
+ deviceData.description,
1112
+ error?.code,
1069
1113
  );
1070
-
1071
- if (
1072
- typeof commandResponse?.sendCommandResponse?.[0]?.traitOperations?.[0]?.event?.event?.cameraEventWindow
1073
- ?.cameraEvent === 'object'
1074
- ) {
1075
- commandResponse.sendCommandResponse[0].traitOperations[0].event.event.cameraEventWindow.cameraEvent.forEach(
1076
- (event) => {
1077
- alerts.push({
1078
- playback_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
1079
- start_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
1080
- end_time: parseInt(event.endTime.seconds) * 1000 + parseInt(event.endTime.nanos) / 1000000,
1081
- id: event.eventId,
1082
- zone_ids:
1083
- typeof event.activityZone === 'object'
1084
- ? event.activityZone.map((zone) =>
1085
- typeof zone?.zoneIndex === 'number' ? zone.zoneIndex : zone.internalIndex,
1086
- )
1087
- : [],
1088
- types: event.eventType
1089
- .map((event) => (event.startsWith('EVENT_') === true ? event.split('EVENT_')[1].toLowerCase() : ''))
1090
- .filter((event) => event),
1091
- });
1092
-
1093
- // Fix up event types to match REST API
1094
- // 'EVENT_UNFAMILIAR_FACE' = 'unfamiliar-face'
1095
- // 'EVENT_PERSON_TALKING' = 'personHeard'
1096
- // 'EVENT_DOG_BARKING' = 'dogBarking'
1097
- // <---- TODO (as the ones we use match from Protobuf)
1098
- },
1099
- );
1100
-
1101
- // Sort alerts to be most recent first
1102
- alerts = alerts.sort((a, b) => {
1103
- if (a.start_time > b.start_time) {
1104
- return -1;
1105
- }
1106
- });
1107
- }
1108
-
1109
- this.#rawData[object.object_key].value.alerts = alerts;
1110
-
1111
- // Send updated data onto HomeKit device for it to process
1112
- this.#eventEmitter.emit(object.object_key, HomeKitDevice.UPDATE, {
1113
- alerts: this.#rawData[object.object_key].value.alerts,
1114
- });
1115
1114
  }
1115
+ });
1116
+ }
1117
+ }, CAMERAZONEPOLLING);
1116
1118
 
1117
- if (
1118
- typeof this.#rawData[object.object_key]?.value === 'object' &&
1119
- this.#rawData[object.object_key]?.source === NestAccfactory.DataSource.REST
1120
- ) {
1121
- let alerts = []; // No alerts yet
1122
- await fetchWrapper(
1123
- 'get',
1124
- this.#rawData[object.object_key].value.nexus_api_http_server_url +
1125
- '/cuepoint/' +
1126
- object.object_key.split('.')[1] +
1127
- '/2?start_time=' +
1128
- Math.floor(Date.now() / 1000 - 30),
1129
- {
1130
- headers: {
1131
- referer: 'https://' + this.#connections[this.#rawData[object.object_key].connection].referer,
1132
- 'User-Agent': USERAGENT,
1133
- [this.#connections[this.#rawData[object.object_key].connection].cameraAPI.key]:
1134
- this.#connections[this.#rawData[object.object_key].connection].cameraAPI.value +
1135
- this.#connections[this.#rawData[object.object_key].connection].cameraAPI.token,
1119
+ // Setup polling loop for camera/doorbell alert data
1120
+ this.#trackedDevices[deviceData.serialNumber].timers.alerts = setInterval(async () => {
1121
+ let alerts = []; // No alerts to processed yet
1122
+ let nest_google_uuid = this.#trackedDevices?.[deviceData?.serialNumber]?.rawDataUuid;
1123
+ if (
1124
+ this.#rawData?.[nest_google_uuid]?.value !== undefined &&
1125
+ this.#trackedDevices?.[deviceData?.serialNumber]?.source === NestAccfactory.DataSource.PROTOBUF
1126
+ ) {
1127
+ let commandResponse = await this.#protobufCommand(this.#rawData[nest_google_uuid].connection, 'ResourceApi', 'SendCommand', {
1128
+ resourceRequest: {
1129
+ resourceId: nest_google_uuid,
1130
+ requestId: crypto.randomUUID(),
1131
+ },
1132
+ resourceCommands: [
1133
+ {
1134
+ traitLabel: 'camera_observation_history',
1135
+ command: {
1136
+ type_url: 'type.nestlabs.com/nest.trait.history.CameraObservationHistoryTrait.CameraObservationHistoryRequest',
1137
+ value: {
1138
+ // We want camera history from now for upto 30secs from now
1139
+ queryStartTime: { seconds: Math.floor(Date.now() / 1000), nanos: (Math.round(Date.now()) % 1000) * 1e6 },
1140
+ queryEndTime: {
1141
+ seconds: Math.floor((Date.now() + 30000) / 1000),
1142
+ nanos: (Math.round(Date.now() + 30000) % 1000) * 1e6,
1136
1143
  },
1137
- timeout: CAMERAALERTPOLLING,
1138
1144
  },
1139
- )
1140
- .then((response) => response.json())
1141
- .then((data) => {
1142
- data.forEach((alert) => {
1143
- // Fix up alert zone IDs. If there is an ID of 0, we'll transform to 1. ie: main zone
1144
- // If there are NO zone IDs, we'll put a 1 in there ie: main zone
1145
- alert.zone_ids = alert.zone_ids.map((id) => (id !== 0 ? id : 1));
1146
- if (alert.zone_ids.length === 0) {
1147
- alert.zone_ids.push(1);
1148
- }
1149
- alerts.push({
1150
- playback_time: alert.playback_time,
1151
- start_time: alert.start_time,
1152
- end_time: alert.end_time,
1153
- id: alert.id,
1154
- zone_ids: alert.zone_ids,
1155
- types: alert.types,
1156
- });
1157
- });
1145
+ },
1146
+ },
1147
+ ],
1148
+ });
1158
1149
 
1159
- // Sort alerts to be most recent first
1160
- alerts = alerts.sort((a, b) => {
1161
- if (a.start_time > b.start_time) {
1162
- return -1;
1163
- }
1164
- });
1165
- })
1166
- .catch((error) => {
1167
- // Log debug message if wasn't a timeout
1168
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
1169
- this.log.debug(
1170
- 'REST API had error retrieving camera/doorbell activity notifications for uuid "%s". Error was "%s"',
1171
- object.object_key,
1172
- error?.code,
1173
- );
1174
- }
1175
- });
1150
+ if (
1151
+ typeof commandResponse?.sendCommandResponse?.[0]?.traitOperations?.[0]?.event?.event?.cameraEventWindow?.cameraEvent ===
1152
+ 'object'
1153
+ ) {
1154
+ commandResponse.sendCommandResponse[0].traitOperations[0].event.event.cameraEventWindow.cameraEvent.forEach((event) => {
1155
+ alerts.push({
1156
+ playback_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
1157
+ start_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
1158
+ end_time: parseInt(event.endTime.seconds) * 1000 + parseInt(event.endTime.nanos) / 1000000,
1159
+ id: event.eventId,
1160
+ zone_ids:
1161
+ typeof event.activityZone === 'object'
1162
+ ? event.activityZone.map((zone) => (zone?.zoneIndex !== undefined ? zone.zoneIndex : zone.internalIndex))
1163
+ : [],
1164
+ types: event.eventType
1165
+ .map((event) => (event.startsWith('EVENT_') === true ? event.split('EVENT_')[1].toLowerCase() : ''))
1166
+ .filter((event) => event),
1167
+ });
1176
1168
 
1177
- this.#rawData[object.object_key].value.alerts = alerts;
1169
+ // Fix up event types to match REST API
1170
+ // 'EVENT_UNFAMILIAR_FACE' = 'unfamiliar-face'
1171
+ // 'EVENT_PERSON_TALKING' = 'personHeard'
1172
+ // 'EVENT_DOG_BARKING' = 'dogBarking'
1173
+ // <---- TODO (as the ones we use match from Protobuf)
1174
+ });
1178
1175
 
1179
- // Send updated data onto HomeKit device for it to process
1180
- this.#eventEmitter.emit(object.object_key, HomeKitDevice.UPDATE, {
1181
- alerts: this.#rawData[object.object_key].value.alerts,
1182
- });
1176
+ // Sort alerts to be most recent first
1177
+ alerts = alerts.sort((a, b) => {
1178
+ if (a.start_time > b.start_time) {
1179
+ return -1;
1183
1180
  }
1184
- }, CAMERAALERTPOLLING);
1181
+ });
1185
1182
  }
1186
1183
  }
1187
- if (deviceData.device_type === NestAccfactory.DeviceType.WEATHER && typeof NestWeather === 'function') {
1188
- // Nest 'Virtual' weather station - Categories.SENSOR = 10
1189
- this?.log?.debug &&
1190
- this.log.debug('Using "%s" data source for "%s"', this.#rawData[object.object_key]?.source, deviceData.description);
1191
- let tempDevice = new NestWeather(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
1192
- tempDevice.add('Nest Weather', 10, true);
1193
-
1194
- // Setup polling loop for weather data if not already created
1195
- if (typeof this.#rawData[object.object_key]?.timers?.weather === 'undefined') {
1196
- this.#rawData[object.object_key].timers.weather = setInterval(async () => {
1197
- this.#rawData[object.object_key].value.weather = await this.#getWeatherData(
1198
- this.#rawData[object.object_key].connection,
1199
- object.object_key,
1200
- this.#rawData[object.object_key].value.weather.latitude,
1201
- this.#rawData[object.object_key].value.weather.longitude,
1202
- );
1203
1184
 
1204
- // Send updated data onto HomeKit device for it to process
1205
- this.#eventEmitter.emit(
1206
- object.object_key,
1207
- HomeKitDevice.UPDATE,
1208
- this.#processData(object.object_key)[deviceData.serial_number],
1209
- );
1210
- }, WEATHERPOLLING);
1211
- }
1212
- }
1213
- }
1214
- });
1185
+ if (
1186
+ this.#rawData?.[nest_google_uuid]?.value !== undefined &&
1187
+ this.#trackedDevices?.[deviceData?.serialNumber]?.source === NestAccfactory.DataSource.REST
1188
+ ) {
1189
+ await fetchWrapper(
1190
+ 'get',
1191
+ this.#rawData[nest_google_uuid].value.nexus_api_http_server_url +
1192
+ '/cuepoint/' +
1193
+ nest_google_uuid.split('.')[1] +
1194
+ '/2?start_time=' +
1195
+ Math.floor(Date.now() / 1000 - 30),
1196
+ {
1197
+ headers: {
1198
+ referer: 'https://' + this.#connections[this.#rawData[nest_google_uuid].connection].referer,
1199
+ 'User-Agent': USERAGENT,
1200
+ [this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.key]:
1201
+ this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.value +
1202
+ this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.token,
1203
+ },
1204
+ timeout: CAMERAALERTPOLLING,
1205
+ retry: 3,
1206
+ },
1207
+ )
1208
+ .then((response) => response.json())
1209
+ .then((data) => {
1210
+ data.forEach((alert) => {
1211
+ // Fix up alert zone IDs. If there is an ID of 0, we'll transform to 1. ie: main zone
1212
+ // If there are NO zone IDs, we'll put a 1 in there ie: main zone
1213
+ alert.zone_ids = alert.zone_ids.map((id) => (id !== 0 ? id : 1));
1214
+ if (alert.zone_ids.length === 0) {
1215
+ alert.zone_ids.push(1);
1216
+ }
1217
+ alerts.push({
1218
+ playback_time: alert.playback_time,
1219
+ start_time: alert.start_time,
1220
+ end_time: alert.end_time,
1221
+ id: alert.id,
1222
+ zone_ids: alert.zone_ids,
1223
+ types: alert.types,
1224
+ });
1225
+ });
1215
1226
 
1216
- // Finally, after processing device additions, if device is not excluded, send updated data to device for it to process
1217
- if (deviceData.excluded === false) {
1218
- this.#eventEmitter.emit(deviceData.uuid, HomeKitDevice.UPDATE, deviceData);
1227
+ // Sort alerts to be most recent first
1228
+ alerts = alerts.sort((a, b) => {
1229
+ if (a.start_time > b.start_time) {
1230
+ return -1;
1231
+ }
1232
+ });
1233
+ })
1234
+ .catch((error) => {
1235
+ // Log debug message if wasn't a timeout
1236
+ if (
1237
+ error?.cause !== undefined &&
1238
+ JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
1239
+ this?.log?.debug
1240
+ ) {
1241
+ this.log.debug(
1242
+ 'REST API had error retrieving camera/doorbell activity notifications for "%s". Error was "%s"',
1243
+ deviceData.description,
1244
+ error?.code,
1245
+ );
1246
+ }
1247
+ });
1248
+ }
1249
+
1250
+ // Update internal structure with new alerts.
1251
+ // We do a test to see if its still present not interval loop not finished or device removed
1252
+ if (this.#rawData?.[nest_google_uuid]?.value !== undefined) {
1253
+ this.#rawData[nest_google_uuid].value.alerts = alerts;
1254
+
1255
+ // Send updated alerts onto HomeKit device for it to process
1256
+ if (this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
1257
+ this.#eventEmitter.emit(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, {
1258
+ alerts: alerts,
1259
+ });
1260
+ }
1261
+ }
1262
+ }, CAMERAALERTPOLLING);
1263
+ }
1264
+
1265
+ if (deviceData.device_type === NestAccfactory.DeviceType.WEATHER && typeof NestWeather === 'function') {
1266
+ // Nest 'Virtual' weather station - Categories.SENSOR = 10
1267
+ let tempDevice = new NestWeather(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
1268
+ tempDevice.add('Nest Weather', 10, true);
1269
+
1270
+ // Track this device once created
1271
+ this.#trackedDevices[deviceData.serialNumber] = {
1272
+ uuid: tempDevice.uuid,
1273
+ rawDataUuid: deviceData.nest_google_uuid,
1274
+ source: undefined,
1275
+ timers: {},
1276
+ };
1277
+
1278
+ // Setup polling loop for weather data
1279
+ this.#trackedDevices[deviceData.serialNumber].timers.weather = setInterval(async () => {
1280
+ let nest_google_uuid = this.#trackedDevices?.[deviceData?.serialNumber]?.rawDataUuid;
1281
+ if (this.#rawData?.[nest_google_uuid] !== undefined) {
1282
+ this.#rawData[nest_google_uuid].value.weather = await this.#getWeatherData(
1283
+ this.#rawData[nest_google_uuid].connection,
1284
+ nest_google_uuid,
1285
+ this.#rawData[nest_google_uuid].value.weather.latitude,
1286
+ this.#rawData[nest_google_uuid].value.weather.longitude,
1287
+ );
1288
+
1289
+ // Send updated weather data onto HomeKit device for it to process
1290
+ if (this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
1291
+ this.#eventEmitter.emit(
1292
+ this.#trackedDevices[deviceData.serialNumber].uuid,
1293
+ HomeKitDevice.UPDATE,
1294
+ this.#processData(nest_google_uuid)[deviceData.serialNumber],
1295
+ );
1296
+ }
1297
+ }
1298
+ }, WEATHERPOLLING);
1299
+ }
1300
+ }
1301
+
1302
+ // Finally, after device data for anything we havent tracked yet, if device is not excluded, send updated data to device for it to process
1303
+ if (deviceData.excluded === false && this.#trackedDevices?.[deviceData?.serialNumber] !== undefined) {
1304
+ if (
1305
+ this.#rawData[deviceData?.nest_google_uuid]?.source !== undefined &&
1306
+ this.#rawData[deviceData.nest_google_uuid].source !== this.#trackedDevices[deviceData.serialNumber].source
1307
+ ) {
1308
+ // Data source for this device has been updated
1309
+ this?.log?.debug &&
1310
+ this.log.debug(
1311
+ 'Using %s API as data source for "%s" from connection uuid "%s"',
1312
+ this.#rawData[deviceData.nest_google_uuid]?.source,
1313
+ deviceData.description,
1314
+ this.#rawData[deviceData.nest_google_uuid].connection,
1315
+ );
1316
+
1317
+ this.#trackedDevices[deviceData.serialNumber].source = this.#rawData[deviceData.nest_google_uuid].source;
1318
+ this.#trackedDevices[deviceData.serialNumber].rawDataUuid = deviceData.nest_google_uuid;
1319
+ }
1320
+
1321
+ this.#eventEmitter.emit(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, deviceData);
1219
1322
  }
1220
1323
  });
1221
1324
  }
@@ -1259,26 +1362,17 @@ export default class NestAccfactory {
1259
1362
  return location;
1260
1363
  };
1261
1364
 
1262
- // Process data for any thermostat(s) we have in the raw data
1263
- const process_thermostat_data = (object_key, data) => {
1365
+ // Process common data for all devices
1366
+ const process_common_data = (object_key, data) => {
1264
1367
  let processed = {};
1265
1368
  try {
1266
1369
  // Fix up data we need to
1267
- data.serial_number = data.serial_number.toUpperCase(); // ensure serial numbers are in upper case
1268
- data.excluded =
1269
- typeof this.config?.devices?.[data.serial_number]?.exclude === 'boolean'
1270
- ? this.config.devices[data.serial_number].exclude
1271
- : false; // Mark device as excluded or not
1272
- data.device_type = NestAccfactory.DeviceType.THERMOSTAT; // Nest Thermostat
1273
- data.uuid = object_key; // Internal structure ID
1370
+ data.nest_google_uuid = object_key;
1371
+ data.serialNumber = data.serialNumber.toUpperCase(); // ensure serial numbers are in upper case
1372
+ data.excluded = this.config?.devices?.[data?.serialNumber]?.exclude === true; // Mark device as excluded or not
1274
1373
  data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
1275
- data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
1276
- data.target_temperature_high = adjustTemperature(data.target_temperature_high, 'C', 'C', true);
1277
- data.target_temperature_low = adjustTemperature(data.target_temperature_low, 'C', 'C', true);
1278
- data.target_temperature = adjustTemperature(data.target_temperature, 'C', 'C', true);
1279
- data.backplate_temperature = adjustTemperature(data.backplate_temperature, 'C', 'C', true);
1280
- data.current_temperature = adjustTemperature(data.current_temperature, 'C', 'C', true);
1281
- data.battery_level = scaleValue(data.battery_level, 3.6, 3.9, 0, 100);
1374
+ data.softwareVersion =
1375
+ typeof data?.softwareVersion === 'string' ? data.softwareVersion.replace(/([a-zA-Z]+)/g, '.').replace(/-/g, '.') : '0.0.0';
1282
1376
  let description = typeof data?.description === 'string' ? data.description : '';
1283
1377
  let location = typeof data?.location === 'string' ? data.location : '';
1284
1378
  if (description === '') {
@@ -1288,26 +1382,20 @@ export default class NestAccfactory {
1288
1382
  data.description = makeHomeKitName(location === '' ? description : description + ' - ' + location);
1289
1383
  delete data.location;
1290
1384
 
1291
- // Insert details for when using HAP-NodeJS library rather than Homebridge
1292
- if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
1385
+ // Insert HomeKit pairing code for when using HAP-NodeJS library rather than Homebridge
1386
+ // Validate the pairing code is in the format of "xxx-xx-xxx" or "xxxx-xxxx"
1387
+ if (
1388
+ new RegExp(/^([0-9]{3}-[0-9]{2}-[0-9]{3})$/).test(this.config?.options?.hkPairingCode) === true ||
1389
+ new RegExp(/^([0-9]{4}-[0-9]{4})$/).test(this.config?.options?.hkPairingCode) === true
1390
+ ) {
1293
1391
  data.hkPairingCode = this.config.options.hkPairingCode;
1294
1392
  }
1295
1393
  if (
1296
- typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
1297
- this.config.devices[data.serial_number].hkPairingCode !== ''
1394
+ new RegExp(/^([0-9]{3}-[0-9]{2}-[0-9]{3})$/).test(this.config?.devices?.[data.serialNumber]?.hkPairingCode) === true ||
1395
+ new RegExp(/^([0-9]{4}-[0-9]{4})$/).test(this.config?.devices?.[data.serialNumber]?.hkPairingCode) === true
1298
1396
  ) {
1299
- data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
1300
- }
1301
- if (data?.hkPairingCode !== undefined && data?.mac_address !== undefined) {
1302
- // Create mac_address in format of xx:xx:xx:xx:xx:xx
1303
- data.hkUsername = data.mac_address
1304
- .toString('hex')
1305
- .split(/(..)/)
1306
- .filter((s) => s)
1307
- .join(':')
1308
- .toUpperCase();
1397
+ data.hkPairingCode = this.config.devices[data.serialNumber].hkPairingCode;
1309
1398
  }
1310
- delete data.mac_address;
1311
1399
 
1312
1400
  processed = data;
1313
1401
  // eslint-disable-next-line no-unused-vars
@@ -1317,6 +1405,27 @@ export default class NestAccfactory {
1317
1405
  return processed;
1318
1406
  };
1319
1407
 
1408
+ // Process data for any thermostat(s) we have in the raw data
1409
+ const process_thermostat_data = (object_key, data) => {
1410
+ let processed = {};
1411
+ try {
1412
+ // Fix up data we need to
1413
+ data = process_common_data(object_key, data);
1414
+ data.device_type = NestAccfactory.DeviceType.THERMOSTAT; // Nest Thermostat
1415
+ data.target_temperature_high = adjustTemperature(data.target_temperature_high, 'C', 'C', true);
1416
+ data.target_temperature_low = adjustTemperature(data.target_temperature_low, 'C', 'C', true);
1417
+ data.target_temperature = adjustTemperature(data.target_temperature, 'C', 'C', true);
1418
+ data.backplate_temperature = adjustTemperature(data.backplate_temperature, 'C', 'C', true);
1419
+ data.current_temperature = adjustTemperature(data.current_temperature, 'C', 'C', true);
1420
+ data.battery_level = scaleValue(data.battery_level, 3.6, 3.9, 0, 100);
1421
+ processed = data;
1422
+ // eslint-disable-next-line no-unused-vars
1423
+ } catch (error) {
1424
+ // Empty
1425
+ }
1426
+ return processed;
1427
+ };
1428
+
1320
1429
  const PROTOBUF_THERMOSTAT_RESOURCES = [
1321
1430
  'nest.resource.NestLearningThermostat3Resource',
1322
1431
  'nest.resource.NestAgateDisplayResource',
@@ -1334,11 +1443,13 @@ export default class NestAccfactory {
1334
1443
  .forEach(([object_key, value]) => {
1335
1444
  let tempDevice = {};
1336
1445
  try {
1337
- if (value?.source === NestAccfactory.DataSource.PROTOBUF) {
1446
+ if (value?.source === NestAccfactory.DataSource.PROTOBUF && value.value?.configuration_done?.deviceReady === true) {
1338
1447
  let RESTTypeData = {};
1339
- RESTTypeData.mac_address = Buffer.from(value.value.wifi_interface.macAddress, 'base64');
1340
- RESTTypeData.serial_number = value.value.device_identity.serialNumber;
1341
- RESTTypeData.software_version = value.value.device_identity.softwareVersion;
1448
+ RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
1449
+ RESTTypeData.softwareVersion =
1450
+ value.value.device_identity.softwareVersion.split(/\s+/)?.[3] !== undefined
1451
+ ? value.value.device_identity.softwareVersion.split(/\s+/)?.[3]
1452
+ : value.value.device_identity.softwareVersion;
1342
1453
  RESTTypeData.model = 'Thermostat';
1343
1454
  if (value.value.device_info.typeName === 'nest.resource.NestLearningThermostat3Resource') {
1344
1455
  RESTTypeData.model = 'Learning Thermostat (3rd gen)';
@@ -1356,8 +1467,8 @@ export default class NestAccfactory {
1356
1467
  RESTTypeData.model = 'Thermostat (2020 Model)';
1357
1468
  }
1358
1469
  RESTTypeData.current_humidity =
1359
- typeof value.value.current_humidity.humidityValue.humidity.value === 'number'
1360
- ? value.value.current_humidity.humidityValue.humidity.value
1470
+ isNaN(value.value.current_humidity.humidityValue.humidity.value) === false
1471
+ ? Number(value.value.current_humidity.humidityValue.humidity.value)
1361
1472
  : 0.0;
1362
1473
  RESTTypeData.temperature_scale = value.value.display_settings.temperatureScale === 'TEMPERATURE_SCALE_F' ? 'F' : 'C';
1363
1474
  RESTTypeData.removed_from_base = value.value.display.thermostatState.includes('bpd') === true;
@@ -1401,25 +1512,25 @@ export default class NestAccfactory {
1401
1512
  ? value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase()
1402
1513
  : 'off';
1403
1514
  RESTTypeData.target_temperature_low =
1404
- typeof value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value === 'number'
1405
- ? value.value.target_temperature_settings.targetTemperature.heatingTarget.value
1515
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value) === false
1516
+ ? Number(value.value.target_temperature_settings.targetTemperature.heatingTarget.value)
1406
1517
  : 0.0;
1407
1518
  RESTTypeData.target_temperature_high =
1408
- typeof value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value === 'number'
1409
- ? value.value.target_temperature_settings.targetTemperature.coolingTarget.value
1519
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value) === false
1520
+ ? Number(value.value.target_temperature_settings.targetTemperature.coolingTarget.value)
1410
1521
  : 0.0;
1411
1522
  RESTTypeData.target_temperature =
1412
1523
  value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'SET_POINT_TYPE_COOL' &&
1413
- typeof value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value === 'number'
1414
- ? value.value.target_temperature_settings.targetTemperature.coolingTarget.value
1524
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value) === false
1525
+ ? Number(value.value.target_temperature_settings.targetTemperature.coolingTarget.value)
1415
1526
  : value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'SET_POINT_TYPE_HEAT' &&
1416
- typeof value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value === 'number'
1417
- ? value.value.target_temperature_settings.targetTemperature.heatingTarget.value
1527
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value) === false
1528
+ ? Number(value.value.target_temperature_settings.targetTemperature.heatingTarget.value)
1418
1529
  : value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'SET_POINT_TYPE_RANGE' &&
1419
- typeof value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value === 'number' &&
1420
- typeof value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value === 'number'
1421
- ? (value.value.target_temperature_settings.targetTemperature.coolingTarget.value +
1422
- value.value.target_temperature_settings.targetTemperature.heatingTarget.value) *
1530
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value) === false &&
1531
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value) === false
1532
+ ? (Number(value.value.target_temperature_settings.targetTemperature.coolingTarget.value) +
1533
+ Number(value.value.target_temperature_settings.targetTemperature.heatingTarget.value)) *
1423
1534
  0.5
1424
1535
  : 0.0;
1425
1536
 
@@ -1478,13 +1589,15 @@ export default class NestAccfactory {
1478
1589
 
1479
1590
  // Update fan status, on or off and max number of speeds supported
1480
1591
  RESTTypeData.fan_state = parseInt(value.value.fan_control_settings.timerEnd?.seconds) > 0 ? true : false;
1481
- RESTTypeData.fan_current_speed =
1482
- value.value.fan_control_settings.timerSpeed.includes('FAN_SPEED_SETTING_STAGE') === true
1483
- ? parseInt(value.value.fan_control_settings.timerSpeed.split('FAN_SPEED_SETTING_STAGE')[1])
1592
+ RESTTypeData.fan_timer_speed =
1593
+ value.value.fan_control_settings.timerSpeed.includes('FAN_SPEED_SETTING_STAGE') === true &&
1594
+ isNaN(value.value.fan_control_settings.timerSpeed.split('FAN_SPEED_SETTING_STAGE')[1]) === false
1595
+ ? Number(value.value.fan_control_settings.timerSpeed.split('FAN_SPEED_SETTING_STAGE')[1])
1484
1596
  : 0;
1485
1597
  RESTTypeData.fan_max_speed =
1486
- value.value.fan_control_capabilities.maxAvailableSpeed.includes('FAN_SPEED_SETTING_STAGE') === true
1487
- ? parseInt(value.value.fan_control_capabilities.maxAvailableSpeed.split('FAN_SPEED_SETTING_STAGE')[1])
1598
+ value.value.fan_control_capabilities.maxAvailableSpeed.includes('FAN_SPEED_SETTING_STAGE') === true &&
1599
+ isNaN(value.value.fan_control_capabilities.maxAvailableSpeed.split('FAN_SPEED_SETTING_STAGE')[1]) === false
1600
+ ? Number(value.value.fan_control_capabilities.maxAvailableSpeed.split('FAN_SPEED_SETTING_STAGE')[1])
1488
1601
  : 0;
1489
1602
 
1490
1603
  // Humidifier/dehumidifier details
@@ -1550,7 +1663,7 @@ export default class NestAccfactory {
1550
1663
  RESTTypeData.schedules[dayofWeekIndex][Object.entries(RESTTypeData.schedules[dayofWeekIndex]).length] = {
1551
1664
  'temp-min': adjustTemperature(schedule.heatingTarget.value, 'C', 'C', true),
1552
1665
  'temp-max': adjustTemperature(schedule.coolingTarget.value, 'C', 'C', true),
1553
- time: typeof schedule.secondsInDay === 'number' ? schedule.secondsInDay : 0,
1666
+ time: isNaN(schedule.secondsInDay) === false ? Number(schedule.secondsInDay) : 0,
1554
1667
  type: RESTTypeData.schedule_mode.toUpperCase(),
1555
1668
  entry_type: 'setpoint',
1556
1669
  };
@@ -1560,22 +1673,21 @@ export default class NestAccfactory {
1560
1673
  tempDevice = process_thermostat_data(object_key, RESTTypeData);
1561
1674
  }
1562
1675
 
1563
- if (value?.source === NestAccfactory.DataSource.REST) {
1676
+ if (value?.source === NestAccfactory.DataSource.REST && value.value?.where_id !== undefined) {
1564
1677
  let RESTTypeData = {};
1565
- RESTTypeData.mac_address = value.value.mac_address;
1566
- RESTTypeData.serial_number = value.value.serial_number;
1567
- RESTTypeData.software_version = value.value.current_version;
1678
+ RESTTypeData.serialNumber = value.value.serial_number;
1679
+ RESTTypeData.softwareVersion = value.value.current_version;
1568
1680
  RESTTypeData.model = 'Thermostat';
1569
- if (value.value.serial_number.serial_number.substring(0, 2) === '15') {
1681
+ if (value.value.serial_number.substring(0, 2) === '15') {
1570
1682
  RESTTypeData.model = 'Thermostat E (1st gen)'; // Nest Thermostat E
1571
1683
  }
1572
- if (value.value.serial_number.serial_number.substring(0, 2) === '09') {
1684
+ if (value.value.serial_number.substring(0, 2) === '09') {
1573
1685
  RESTTypeData.model = 'Thermostat (3rd gen)'; // Nest Thermostat 3rd Gen
1574
1686
  }
1575
- if (value.value.serial_number.serial_number.substring(0, 2) === '02') {
1687
+ if (value.value.serial_number.substring(0, 2) === '02') {
1576
1688
  RESTTypeData.model = 'Thermostat (2nd gen)'; // Nest Thermostat 2nd Gen
1577
1689
  }
1578
- if (value.value.serial_number.serial_number.substring(0, 2) === '01') {
1690
+ if (value.value.serial_number.substring(0, 2) === '01') {
1579
1691
  RESTTypeData.model = 'Thermostat (1st gen)'; // Nest Thermostat 1st Gen
1580
1692
  }
1581
1693
  RESTTypeData.current_humidity = value.value.current_humidity;
@@ -1682,13 +1794,17 @@ export default class NestAccfactory {
1682
1794
 
1683
1795
  // Update fan status, on or off
1684
1796
  RESTTypeData.fan_state = value.value.fan_timer_timeout > 0 ? true : false;
1685
- RESTTypeData.fan_current_speed =
1686
- value.value.fan_timer_speed.includes('stage') === true ? parseInt(value.value.fan_timer_speed.split('stage')[1]) : 0;
1797
+ RESTTypeData.fan_timer_speed =
1798
+ value.value.fan_timer_speed.includes('stage') === true && isNaN(value.value.fan_timer_speed.split('stage')[1]) === false
1799
+ ? Number(value.value.fan_timer_speed.split('stage')[1])
1800
+ : 0;
1687
1801
  RESTTypeData.fan_max_speed =
1688
- value.value.fan_capabilities.includes('stage') === true ? parseInt(value.value.fan_capabilities.split('stage')[1]) : 0;
1802
+ value.value.fan_capabilities.includes('stage') === true && isNaN(value.value.fan_capabilities.split('stage')[1]) === false
1803
+ ? Number(value.value.fan_capabilities.split('stage')[1])
1804
+ : 0;
1689
1805
 
1690
1806
  // Humidifier/dehumidifier details
1691
- RESTTypeData.target_humidity = typeof value.value.target_humidity === 'number' ? value.value.target_humidity : 0.0;
1807
+ RESTTypeData.target_humidity = isNaN(value.value.target_humidity) === false ? Number(value.value.target_humidity) : 0.0;
1692
1808
  RESTTypeData.humidifier_state = value.value.humidifier_state === true;
1693
1809
  RESTTypeData.dehumidifier_state = value.value.dehumidifier_state === true;
1694
1810
 
@@ -1722,14 +1838,14 @@ export default class NestAccfactory {
1722
1838
  Object.values(this.#rawData['schedule.' + value.value.serial_number].value.days).forEach((schedules) => {
1723
1839
  Object.values(schedules).forEach((schedule) => {
1724
1840
  // Fix up temperatures in the schedule
1725
- if (typeof schedule['temp'] === 'number') {
1726
- schedule.temp = adjustTemperature(schedule.temp, 'C', 'C', true);
1841
+ if (isNaN(schedule['temp']) === false) {
1842
+ schedule.temp = adjustTemperature(Number(schedule.temp), 'C', 'C', true);
1727
1843
  }
1728
- if (typeof schedule['temp-min'] === 'number') {
1729
- schedule['temp-min'] = adjustTemperature(schedule['temp-min'], 'C', 'C', true);
1844
+ if (isNaN(schedule['temp-min']) === false) {
1845
+ schedule['temp-min'] = adjustTemperature(Number(schedule['temp-min']), 'C', 'C', true);
1730
1846
  }
1731
- if (typeof schedule['temp-max'] === 'number') {
1732
- schedule['temp-max'] = adjustTemperature(schedule['temp-max'], 'C', 'C', true);
1847
+ if (isNaN(schedule['temp-max']) === false) {
1848
+ schedule['temp-max'] = adjustTemperature(Number(schedule['temp-max']), 'C', 'C', true);
1733
1849
  }
1734
1850
  });
1735
1851
  });
@@ -1744,28 +1860,28 @@ export default class NestAccfactory {
1744
1860
  this?.log?.debug && this.log.debug('Error processing data for thermostat(s)');
1745
1861
  }
1746
1862
 
1747
- if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
1863
+ if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
1748
1864
  // Insert any extra options we've read in from configuration file for this device
1749
1865
  tempDevice.eveHistory =
1750
- this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
1751
- tempDevice.humiditySensor = this.config?.devices?.[tempDevice.serial_number]?.humiditySensor === true;
1866
+ this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serialNumber]?.eveHistory === true;
1867
+ tempDevice.humiditySensor = this.config?.devices?.[tempDevice.serialNumber]?.humiditySensor === true;
1752
1868
  tempDevice.externalCool =
1753
- typeof this.config?.devices?.[tempDevice.serial_number]?.externalCool === 'string'
1754
- ? this.config.devices[tempDevice.serial_number].externalCool
1869
+ typeof this.config?.devices?.[tempDevice.serialNumber]?.externalCool === 'string'
1870
+ ? this.config.devices[tempDevice.serialNumber].externalCool
1755
1871
  : undefined; // Config option for external cooling source
1756
1872
  tempDevice.externalHeat =
1757
- typeof this.config?.devices?.[tempDevice.serial_number]?.externalHeat === 'string'
1758
- ? this.config.devices[tempDevice.serial_number].externalHeat
1873
+ typeof this.config?.devices?.[tempDevice.serialNumber]?.externalHeat === 'string'
1874
+ ? this.config.devices[tempDevice.serialNumber].externalHeat
1759
1875
  : undefined; // Config option for external heating source
1760
1876
  tempDevice.externalFan =
1761
- typeof this.config?.devices?.[tempDevice.serial_number]?.externalFan === 'string'
1762
- ? this.config.devices[tempDevice.serial_number].externalFan
1877
+ typeof this.config?.devices?.[tempDevice.serialNumber]?.externalFan === 'string'
1878
+ ? this.config.devices[tempDevice.serialNumber].externalFan
1763
1879
  : undefined; // Config option for external fan source
1764
1880
  tempDevice.externalDehumidifier =
1765
- typeof this.config?.devices?.[tempDevice.serial_number]?.externalDehumidifier === 'string'
1766
- ? this.config.devices[tempDevice.serial_number].externalDehumidifier
1881
+ typeof this.config?.devices?.[tempDevice.serialNumber]?.externalDehumidifier === 'string'
1882
+ ? this.config.devices[tempDevice.serialNumber].externalDehumidifier
1767
1883
  : undefined; // Config option for external dehumidifier source
1768
- devices[tempDevice.serial_number] = tempDevice; // Store processed device
1884
+ devices[tempDevice.serialNumber] = tempDevice; // Store processed device
1769
1885
  }
1770
1886
  });
1771
1887
 
@@ -1776,48 +1892,10 @@ export default class NestAccfactory {
1776
1892
  let processed = {};
1777
1893
  try {
1778
1894
  // Fix up data we need to
1779
- data.serial_number = data.serial_number.toUpperCase();
1780
- data.excluded =
1781
- typeof this.config?.devices?.[data.serial_number]?.exclude === 'boolean'
1782
- ? this.config.devices[data.serial_number].exclude
1783
- : false; // Mark device as excluded or not
1895
+ data = process_common_data(object_key, data);
1784
1896
  data.device_type = NestAccfactory.DeviceType.TEMPSENSOR; // Nest Temperature sensor
1785
- data.uuid = object_key; // Internal structure ID
1786
- data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
1787
- data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
1788
1897
  data.model = 'Temperature Sensor';
1789
1898
  data.current_temperature = adjustTemperature(data.current_temperature, 'C', 'C', true);
1790
- let description = typeof data?.description === 'string' ? data.description : '';
1791
- let location = typeof data?.location === 'string' ? data.location : '';
1792
- if (description === '') {
1793
- description = location;
1794
- location = '';
1795
- }
1796
- data.description = makeHomeKitName(location === '' ? description : description + ' - ' + location);
1797
- delete data.location;
1798
-
1799
- // Insert details for when using HAP-NodeJS library rather than Homebridge
1800
- if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
1801
- data.hkPairingCode = this.config.options.hkPairingCode;
1802
- }
1803
- if (
1804
- typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
1805
- this.config.devices[data.serial_number].hkPairingCode !== ''
1806
- ) {
1807
- data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
1808
- }
1809
- if (data?.hkPairingCode !== undefined) {
1810
- // Use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off serial number for last 6 digits.
1811
- let tempMACAddress = '18B430' + crc24(data.serial_number).toUpperCase();
1812
- data.hkUsername = tempMACAddress
1813
- .toString('hex')
1814
- .split(/(..)/)
1815
- .filter((s) => s)
1816
- .join(':')
1817
- .toUpperCase(); // Create mac_address in format of xx:xx:xx:xx:xx:xx
1818
- }
1819
- delete data.mac_address;
1820
-
1821
1899
  processed = data;
1822
1900
  // eslint-disable-next-line no-unused-vars
1823
1901
  } catch (error) {
@@ -1838,13 +1916,14 @@ export default class NestAccfactory {
1838
1916
  try {
1839
1917
  if (
1840
1918
  value?.source === NestAccfactory.DataSource.PROTOBUF &&
1919
+ value.value?.configuration_done?.deviceReady === true &&
1841
1920
  typeof value?.value?.associated_thermostat === 'string' &&
1842
1921
  value?.value?.associated_thermostat !== ''
1843
1922
  ) {
1844
1923
  let RESTTypeData = {};
1845
- RESTTypeData.serial_number = value.value.device_identity.serialNumber;
1924
+ RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
1846
1925
  // Guessing battery minimum voltage is 2v??
1847
- RESTTypeData.battery_level = scaleValue(value.value.battery.assessedVoltage.value, 2.0, 3.0, 0, 100);
1926
+ RESTTypeData.battery_level = scaleValue(Number(value.value.battery.assessedVoltage.value), 2.0, 3.0, 0, 100);
1848
1927
  RESTTypeData.current_temperature = value.value.current_temperature.temperatureValue.temperature.value;
1849
1928
  // Online status we 'faked' when processing Thermostat Protobuf data
1850
1929
  RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
@@ -1861,12 +1940,14 @@ export default class NestAccfactory {
1861
1940
  }
1862
1941
  if (
1863
1942
  value?.source === NestAccfactory.DataSource.REST &&
1943
+ value.value?.where_id !== undefined &&
1944
+ value.value?.structure_id !== undefined &&
1864
1945
  typeof value?.value?.associated_thermostat === 'string' &&
1865
1946
  value?.value?.associated_thermostat !== ''
1866
1947
  ) {
1867
1948
  let RESTTypeData = {};
1868
- RESTTypeData.serial_number = value.value.sserial_number;
1869
- RESTTypeData.battery_level = scaleValue(value.value.battery_level, 0, 100, 0, 100);
1949
+ RESTTypeData.serialNumber = value.value.serial_number;
1950
+ RESTTypeData.battery_level = scaleValue(Number(value.value.battery_level), 0, 100, 0, 100);
1870
1951
  RESTTypeData.current_temperature = value.value.current_temperature;
1871
1952
  RESTTypeData.online = Math.floor(Date.now() / 1000) - value.value.last_updated_at < 3600 * 4 ? true : false;
1872
1953
  RESTTypeData.associated_thermostat = value.value.associated_thermostat;
@@ -1881,11 +1962,11 @@ export default class NestAccfactory {
1881
1962
  } catch (error) {
1882
1963
  this?.log?.debug && this.log.debug('Error processing data for temperature sensor(s)');
1883
1964
  }
1884
- if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
1965
+ if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
1885
1966
  // Insert any extra options we've read in from configuration file for this device
1886
1967
  tempDevice.eveHistory =
1887
- this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
1888
- devices[tempDevice.serial_number] = tempDevice; // Store processed device
1968
+ this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serialNumber]?.eveHistory === true;
1969
+ devices[tempDevice.serialNumber] = tempDevice; // Store processed device
1889
1970
  }
1890
1971
  });
1891
1972
 
@@ -1894,15 +1975,8 @@ export default class NestAccfactory {
1894
1975
  let processed = {};
1895
1976
  try {
1896
1977
  // Fix up data we need to
1897
- data.serial_number = data.serial_number.toUpperCase(); // ensure serial numbers are in upper case
1898
- data.excluded =
1899
- typeof this.config?.devices?.[data.serial_number]?.exclude === 'boolean'
1900
- ? this.config.devices[data.serial_number].exclude
1901
- : false; // Mark device as excluded or not
1978
+ data = process_common_data(object_key, data);
1902
1979
  data.device_type = NestAccfactory.DeviceType.SMOKESENSOR; // Nest Protect
1903
- data.uuid = object_key; // Internal structure ID
1904
- data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
1905
- data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
1906
1980
  data.battery_level = scaleValue(data.battery_level, 0, 5400, 0, 100);
1907
1981
  data.model = 'Protect';
1908
1982
  if (data.wired_or_battery === 0) {
@@ -1911,42 +1985,12 @@ export default class NestAccfactory {
1911
1985
  if (data.wired_or_battery === 1) {
1912
1986
  data.model = data.model + ' (battery'; // Battery powered
1913
1987
  }
1914
- if (data.serial_number.substring(0, 2) === '06') {
1988
+ if (data.serialNumber.substring(0, 2) === '06') {
1915
1989
  data.model = data.model + ', 2nd gen)'; // Nest Protect 2nd Gen
1916
1990
  }
1917
- if (data.serial_number.substring(0, 2) === '05') {
1991
+ if (data.serialNumber.substring(0, 2) === '05') {
1918
1992
  data.model = data.model + ', 1st gen)'; // Nest Protect 1st Gen
1919
1993
  }
1920
- let description = typeof data?.description === 'string' ? data.description : '';
1921
- let location = typeof data?.location === 'string' ? data.location : '';
1922
- if (description === '') {
1923
- description = location;
1924
- location = '';
1925
- }
1926
- data.description = makeHomeKitName(location === '' ? description : description + ' - ' + location);
1927
- delete data.location;
1928
-
1929
- // Insert details for when using HAP-NodeJS library rather than Homebridge
1930
- if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
1931
- data.hkPairingCode = this.config.options.hkPairingCode;
1932
- }
1933
- if (
1934
- typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
1935
- this.config.devices[data.serial_number].hkPairingCode !== ''
1936
- ) {
1937
- data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
1938
- }
1939
- if (data?.hkPairingCode !== undefined && data?.mac_address !== undefined) {
1940
- // Create mac_address in format of xx:xx:xx:xx:xx:xx
1941
- data.hkUsername = data.mac_address
1942
- .toString('hex')
1943
- .split(/(..)/)
1944
- .filter((s) => s)
1945
- .join(':')
1946
- .toUpperCase();
1947
- }
1948
- delete data.mac_address;
1949
-
1950
1994
  processed = data;
1951
1995
  // eslint-disable-next-line no-unused-vars
1952
1996
  } catch (error) {
@@ -1965,57 +2009,18 @@ export default class NestAccfactory {
1965
2009
  .forEach(([object_key, value]) => {
1966
2010
  let tempDevice = {};
1967
2011
  try {
1968
- if (value?.source === NestAccfactory.DataSource.PROTOBUF) {
1969
- /*
1970
- let RESTTypeData = {};
1971
- RESTTypeData.mac_address = Buffer.from(value.value.wifi_interface.macAddress, 'base64');
1972
- RESTTypeData.serial_number = value.value.device_identity.serialNumber;
1973
- RESTTypeData.software_version = value.value.device_identity.softwareVersion;
1974
- RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
1975
- RESTTypeData.line_power_present = value.value?.wall_power?.status === 'POWER_SOURCE_STATUS_ACTIVE';
1976
- RESTTypeData.wired_or_battery = typeof value.value?.wall_power === 'object' ? 0 : 1;
1977
- RESTTypeData.battery_level = parseFloat(value.value.battery_voltage_bank1.batteryValue.batteryVoltage.value);
1978
- RESTTypeData.battery_health_state = value.value.battery_voltage_bank1.faultInformation;
1979
- RESTTypeData.smoke_status = value.value.safety_alarm_smoke.alarmState === 'ALARM_STATE_ALARM' ? 2 : 0; // matches REST data
1980
- RESTTypeData.co_status = value.value.safety_alarm_co.alarmState === 'ALARM_STATE_ALARM' ? 2 : 0; // matches REST data
1981
- // RESTTypeData.heat_status =
1982
- RESTTypeData.hushed_state =
1983
- value.value.safety_alarm_smoke.silenceState === 'SILENCE_STATE_SILENCED' ||
1984
- value.value.safety_alarm_co.silenceState === 'SILENCE_STATE_SILENCED';
1985
- RESTTypeData.ntp_green_led_enable = value.value.night_time_promise_settings.greenLedEnabled === true;
1986
- RESTTypeData.smoke_test_passed = value.value.safety_summary.warningDevices.failures.includes('FAILURE_TYPE_SMOKE') === false;
1987
- RESTTypeData.heat_test_passed = value.value.safety_summary.warningDevices.failures.includes('FAILURE_TYPE_TEMP') === false;
1988
- RESTTypeData.latest_alarm_test =
1989
- parseInt(value.value.self_test.lastMstEnd?.second) > 0 ? parseInt(value.value.self_test.lastMstEnd.seconds) : 0;
1990
- RESTTypeData.self_test_in_progress =
1991
- value.value.legacy_structure_self_test.mstInProgress === true ||
1992
- value.value.legacy_structure_self_test.astInProgress === true;
1993
- RESTTypeData.replacement_date =
1994
- value.value.legacy_protect_device_settings.replaceByDate.hasOwnProperty('seconds') === true
1995
- ? parseInt(value.value.legacy_protect_device_settings.replaceByDate.seconds)
1996
- : 0;
1997
-
1998
- // RESTTypeData.removed_from_base =
1999
- RESTTypeData.topaz_hush_key =
2000
- typeof value.value.safety_structure_settings.structureHushKey === 'string'
2001
- ? value.value.safety_structure_settings.structureHushKey
2002
- : '';
2003
- RESTTypeData.detected_motion = value.value.legacy_protect_device_info.autoAway === false;
2004
- RESTTypeData.description = typeof value.value?.label?.label === 'string' ? value.value.label.label : '';
2005
- RESTTypeData.location = get_location_name(
2006
- value.value?.device_info?.pairerId?.resourceId,
2007
- value.value?.device_located_settings?.whereAnnotationRid?.resourceId,
2008
- );
2009
- tempDevice = process_protect_data(object_key, RESTTypeData);
2010
- */
2011
- }
2012
-
2013
- if (value?.source === NestAccfactory.DataSource.REST) {
2012
+ if (
2013
+ value?.source === NestAccfactory.DataSource.REST &&
2014
+ value.value?.where_id !== undefined &&
2015
+ value.value?.structure_id !== undefined
2016
+ ) {
2014
2017
  let RESTTypeData = {};
2015
- RESTTypeData.mac_address = value.value.wifi_mac_address;
2016
- RESTTypeData.serial_number = value.value.serial_number;
2017
- RESTTypeData.software_version = value.value.software_version;
2018
- RESTTypeData.online = this.#rawData?.['widget_track.' + value?.value?.thread_mac_address.toUpperCase()]?.value?.online === true;
2018
+ RESTTypeData.serialNumber = value.value.serial_number;
2019
+ RESTTypeData.softwareVersion = value.value.software_version;
2020
+ RESTTypeData.online =
2021
+ typeof value?.value?.thread_mac_address === 'string'
2022
+ ? this.#rawData?.['widget_track.' + value?.value?.thread_mac_address.toUpperCase()]?.value?.online === true
2023
+ : false;
2019
2024
  RESTTypeData.line_power_present = value.value.line_power_present === true;
2020
2025
  RESTTypeData.wired_or_battery = value.value.wired_or_battery;
2021
2026
  RESTTypeData.battery_level = value.value.battery_level;
@@ -2046,11 +2051,11 @@ export default class NestAccfactory {
2046
2051
  this?.log?.debug && this.log.debug('Error processing data for smoke sensor(s)');
2047
2052
  }
2048
2053
 
2049
- if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
2054
+ if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
2050
2055
  // Insert any extra options we've read in from configuration file for this device
2051
2056
  tempDevice.eveHistory =
2052
- this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
2053
- devices[tempDevice.serial_number] = tempDevice; // Store processed device
2057
+ this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serialNumber]?.eveHistory === true;
2058
+ devices[tempDevice.serialNumber] = tempDevice; // Store processed device
2054
2059
  }
2055
2060
  });
2056
2061
 
@@ -2059,11 +2064,7 @@ export default class NestAccfactory {
2059
2064
  let processed = {};
2060
2065
  try {
2061
2066
  // Fix up data we need to
2062
- data.serial_number = data.serial_number.toUpperCase(); // ensure serial numbers are in upper case
2063
- data.excluded =
2064
- typeof this.config?.devices?.[data.serial_number]?.exclude === 'boolean'
2065
- ? this.config.devices[data.serial_number].exclude
2066
- : false; // Mark device as excluded or not
2067
+ data = process_common_data(object_key, data);
2067
2068
  data.device_type = NestAccfactory.DeviceType.CAMERA;
2068
2069
  if (data.model.toUpperCase().includes('DOORBELL') === true) {
2069
2070
  data.device_type = NestAccfactory.DeviceType.DOORBELL;
@@ -2071,39 +2072,6 @@ export default class NestAccfactory {
2071
2072
  if (data.model.toUpperCase().includes('FLOODLIGHT') === true) {
2072
2073
  data.device_type = NestAccfactory.DeviceType.FLOODLIGHT;
2073
2074
  }
2074
- data.uuid = object_key; // Internal structure ID
2075
- data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
2076
- data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
2077
- let description = typeof data?.description === 'string' ? data.description : '';
2078
- let location = typeof data?.location === 'string' ? data.location : '';
2079
- if (description === '') {
2080
- description = location;
2081
- location = '';
2082
- }
2083
- data.description = makeHomeKitName(location === '' ? description : description + ' - ' + location);
2084
- delete data.location;
2085
-
2086
- // Insert details for when using HAP-NodeJS library rather than Homebridge
2087
- if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
2088
- data.hkPairingCode = this.config.options.hkPairingCode;
2089
- }
2090
- if (
2091
- typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
2092
- this.config.devices[data.serial_number].hkPairingCode !== ''
2093
- ) {
2094
- data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
2095
- }
2096
- if (data?.hkPairingCode !== undefined && data?.mac_address !== undefined) {
2097
- // Create mac_address in format of xx:xx:xx:xx:xx:xx
2098
- data.hkUsername = data.mac_address
2099
- .toString('hex')
2100
- .split(/(..)/)
2101
- .filter((s) => s)
2102
- .join(':')
2103
- .toUpperCase();
2104
- }
2105
- delete data.mac_address;
2106
-
2107
2075
  processed = data;
2108
2076
  // eslint-disable-next-line no-unused-vars
2109
2077
  } catch (error) {
@@ -2134,18 +2102,22 @@ export default class NestAccfactory {
2134
2102
  .forEach(([object_key, value]) => {
2135
2103
  let tempDevice = {};
2136
2104
  try {
2137
- if (value?.source === NestAccfactory.DataSource.PROTOBUF && value.value?.streaming_protocol !== undefined) {
2105
+ if (
2106
+ value?.source === NestAccfactory.DataSource.PROTOBUF &&
2107
+ value.value?.streaming_protocol?.supportedProtocols !== undefined &&
2108
+ (value.value?.configuration_done?.deviceReady === true ||
2109
+ value.value?.camera_migration_status?.state?.where === 'MIGRATED_TO_GOOGLE_HOME')
2110
+ ) {
2138
2111
  let RESTTypeData = {};
2139
- // If we haven't found a macaddress, ase a Nest Labs prefix for first 6 digits followed by a CRC24 based off serial number for last 6 digits.
2140
- RESTTypeData.mac_address =
2141
- value.value?.wifi_interface?.macAddress !== undefined
2142
- ? Buffer.from(value.value.wifi_interface.macAddress, 'base64')
2143
- : '18B430' + crc24(value.value.device_identity.serialNumber.toUpperCase());
2144
- RESTTypeData.serial_number = value.value.device_identity.serialNumber;
2145
- RESTTypeData.software_version =
2146
- value.value?.floodlight_settings?.associatedFloodlightFirmwareVersion !== undefined
2147
- ? value.value.floodlight_settings.associatedFloodlightFirmwareVersion
2148
- : value.value.device_identity.softwareVersion.replace(/[^0-9.]/g, '');
2112
+ RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
2113
+
2114
+ // Protobuf camera firmware versions appear in alonger string. Seems the '4th' word in the string is the actual number version
2115
+ // ie: rquartz-user 1 OPENMASTER 507800056 test-keys stable-channel stable-channel
2116
+ // ie: nq-user 1.73 OPENMASTER 422270 release-keys stable-channel stable-channel
2117
+ RESTTypeData.softwareVersion =
2118
+ value.value.device_identity.softwareVersion.split(/\s+/)?.[3] !== undefined
2119
+ ? value.value.device_identity.softwareVersion.split(/\s+/)?.[3]
2120
+ : value.value.device_identity.softwareVersion;
2149
2121
  RESTTypeData.model = 'Camera';
2150
2122
  if (
2151
2123
  value.value.device_info.typeName === 'google.resource.NeonQuartzResource' &&
@@ -2200,13 +2172,13 @@ export default class NestAccfactory {
2200
2172
  //RESTTypeData.has_statusled =
2201
2173
  //RESTTypeData.statusled_brightness =
2202
2174
  RESTTypeData.has_microphone = value.value?.microphone_settings?.enableMicrophone === true;
2203
- RESTTypeData.has_speaker = value.value?.speaker_volume?.volume === true;
2175
+ RESTTypeData.has_speaker = value.value?.speaker_volume?.volume !== undefined;
2204
2176
  RESTTypeData.has_motion_detection = value.value?.observation_trigger_capabilities?.videoEventTypes?.motion?.value === true;
2205
2177
  RESTTypeData.activity_zones = [];
2206
2178
  if (value.value?.activity_zone_settings?.activityZones !== undefined) {
2207
2179
  value.value.activity_zone_settings.activityZones.forEach((zone) => {
2208
2180
  RESTTypeData.activity_zones.push({
2209
- id: typeof zone.zoneProperties?.zoneId === 'number' ? zone.zoneProperties.zoneId : zone.zoneProperties.internalIndex,
2181
+ id: zone.zoneProperties?.zoneId !== undefined ? zone.zoneProperties.zoneId : zone.zoneProperties.internalIndex,
2210
2182
  name: makeHomeKitName(zone.zoneProperties?.name !== undefined ? zone.zoneProperties.name : ''),
2211
2183
  hidden: false,
2212
2184
  uri: '',
@@ -2228,18 +2200,33 @@ export default class NestAccfactory {
2228
2200
  RESTTypeData.light_enabled = value.value?.floodlight_state?.currentState === 'LIGHT_STATE_ON';
2229
2201
  RESTTypeData.light_brightness =
2230
2202
  value.value?.floodlight_settings?.brightness !== undefined
2231
- ? scaleValue(value.value.floodlight_settings.brightness, 0, 10, 0, 100)
2203
+ ? scaleValue(Number(value.value.floodlight_settings.brightness), 0, 10, 0, 100)
2232
2204
  : 0;
2233
2205
 
2206
+ // Status of where the device sites between Nest/Google Home App
2207
+ RESTTypeData.migrating =
2208
+ value.value?.camera_migration_status?.state?.progress !== undefined &&
2209
+ value.value?.camera_migration_status?.state?.progress !== 'PROGRESS_COMPLETE' &&
2210
+ value.value?.camera_migration_status?.state?.progress !== 'PROGRESS_NONE';
2211
+
2212
+ // Details to allow access to camera API calls for the device
2213
+ RESTTypeData.apiAccess = this.#connections?.[value.connection]?.cameraAPI;
2214
+
2234
2215
  tempDevice = process_camera_doorbell_data(object_key, RESTTypeData);
2235
2216
  }
2236
2217
 
2237
- if (value?.source === NestAccfactory.DataSource.REST && value.value?.properties?.['cc2migration.overview_state'] === 'NORMAL') {
2218
+ if (
2219
+ value?.source === NestAccfactory.DataSource.REST &&
2220
+ value.value?.where_id !== undefined &&
2221
+ value.value?.structure_id !== undefined &&
2222
+ value.value?.nexus_api_http_server_url !== undefined &&
2223
+ (value.value?.properties?.['cc2migration.overview_state'] === 'NORMAL' ||
2224
+ value.value?.properties?.['cc2migration.overview_state'] === 'REVERSE_MIGRATION_IN_PROGRESS')
2225
+ ) {
2238
2226
  // We'll only use the REST API data for Camera's which have NOT been migrated to Google Home
2239
2227
  let RESTTypeData = {};
2240
- RESTTypeData.mac_address = value.value.mac_address;
2241
- RESTTypeData.serial_number = value.value.serial_number;
2242
- RESTTypeData.software_version = value.value.software_version;
2228
+ RESTTypeData.serialNumber = value.value.serial_number;
2229
+ RESTTypeData.softwareVersion = value.value.software_version;
2243
2230
  RESTTypeData.model = value.value.model.replace(/nest\s*/gi, ''); // Use camera/doorbell model that Nest supplies
2244
2231
  RESTTypeData.description = value.value?.description;
2245
2232
  RESTTypeData.location = get_location_name(value.value.structure_id, value.value.where_id);
@@ -2254,7 +2241,10 @@ export default class NestAccfactory {
2254
2241
  RESTTypeData.has_statusled = value.value?.capabilities.includes('statusled') === true;
2255
2242
  RESTTypeData.has_video_flip = value.value?.capabilities.includes('video.flip') === true;
2256
2243
  RESTTypeData.video_flipped = value.value?.properties['video.flipped'] === true;
2257
- RESTTypeData.statusled_brightness = value.value?.properties['statusled.brightness'];
2244
+ RESTTypeData.statusled_brightness =
2245
+ isNaN(value.value?.properties?.['statusled.brightness']) === false
2246
+ ? Number(value.value?.properties['statusled.brightness'])
2247
+ : 0;
2258
2248
  RESTTypeData.has_microphone = value.value?.capabilities.includes('audio.microphone') === true;
2259
2249
  RESTTypeData.has_speaker = value.value?.capabilities.includes('audio.speaker') === true;
2260
2250
  RESTTypeData.has_motion_detection = value.value?.capabilities.includes('detectors.on_camera') === true;
@@ -2265,6 +2255,14 @@ export default class NestAccfactory {
2265
2255
  RESTTypeData.quiet_time_enabled = false;
2266
2256
  RESTTypeData.camera_type = value.value.camera_type;
2267
2257
 
2258
+ // Active migration status between Nest/Google Home App
2259
+ RESTTypeData.migrating =
2260
+ value.value?.properties?.['cc2migration.overview_state'] !== undefined &&
2261
+ value.value?.properties?.['cc2migration.overview_state'] !== 'NORMAL';
2262
+
2263
+ // Details to allow access to camera API calls for the device
2264
+ RESTTypeData.apiAccess = this.#connections?.[value.connection]?.cameraAPI;
2265
+
2268
2266
  tempDevice = process_camera_doorbell_data(object_key, RESTTypeData);
2269
2267
  }
2270
2268
  // eslint-disable-next-line no-unused-vars
@@ -2272,34 +2270,34 @@ export default class NestAccfactory {
2272
2270
  this?.log?.debug && this.log.debug('Error processing data for camera/doorbell(s)');
2273
2271
  }
2274
2272
 
2275
- if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
2276
- // Insert details to allow access to camera API calls for the device
2277
- if (value.connection !== undefined && typeof this.#connections?.[value.connection]?.cameraAPI === 'object') {
2278
- tempDevice.apiAccess = this.#connections[value.connection].cameraAPI;
2279
- }
2280
-
2273
+ if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
2281
2274
  // Insert any extra options we've read in from configuration file for this device
2282
2275
  tempDevice.eveHistory =
2283
- this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
2284
- tempDevice.hksv = this.config.options.hksv === true || this.config?.devices?.[tempDevice.serial_number]?.hksv === true;
2276
+ this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serialNumber]?.eveHistory === true;
2277
+ tempDevice.hksv = this.config.options.hksv === true || this.config?.devices?.[tempDevice.serialNumber]?.hksv === true;
2285
2278
  tempDevice.doorbellCooldown =
2286
- typeof this.config?.devices?.[tempDevice.serial_number]?.doorbellCooldown === 'number'
2287
- ? this.config.devices[tempDevice.serial_number].doorbellCooldown
2279
+ isNaN(this.config?.devices?.[tempDevice.serialNumber]?.doorbellCooldown) === false
2280
+ ? Number(this.config.devices[tempDevice.serialNumber].doorbellCooldown)
2288
2281
  : 60;
2289
2282
  tempDevice.motionCooldown =
2290
- typeof this.config?.devices?.[tempDevice.serial_number]?.motionCooldown === 'number'
2291
- ? this.config.devices[tempDevice.serial_number].motionCooldown
2283
+ isNaN(this.config?.devices?.[tempDevice.serialNumber]?.motionCooldown) === false
2284
+ ? Number(this.config.devices[tempDevice.serialNumber].motionCooldown)
2292
2285
  : 60;
2293
2286
  tempDevice.personCooldown =
2294
- typeof this.config?.devices?.[tempDevice.serial_number]?.personCooldown === 'number'
2295
- ? this.config.devices[tempDevice.serial_number].personCooldown
2287
+ isNaN(this.config?.devices?.[tempDevice.serialNumber]?.personCooldown) === false
2288
+ ? Number(this.config.devices[tempDevice.serialNumber].personCooldown)
2296
2289
  : 120;
2297
- tempDevice.chimeSwitch = this.config?.devices?.[tempDevice.serial_number]?.chimeSwitch === true; // Control 'indoor' chime by switch
2298
- tempDevice.localAccess = this.config?.devices?.[tempDevice.serial_number]?.localAccess === true; // Local network video streaming rather than from cloud from camera/doorbells
2290
+ tempDevice.chimeSwitch = this.config?.devices?.[tempDevice.serialNumber]?.chimeSwitch === true; // Control 'indoor' chime by switch
2291
+ tempDevice.localAccess = this.config?.devices?.[tempDevice.serialNumber]?.localAccess === true; // Local network video streaming rather than from cloud from camera/doorbells
2299
2292
  tempDevice.ffmpeg = this.config.options.ffmpeg; // ffmpeg details, path, libraries. No ffmpeg = undefined
2300
2293
  tempDevice.maxStreams =
2301
- typeof this.config.options?.maxStreams === 'number' ? this.config.options.maxStreams : this.deviceData.hksv === true ? 1 : 2;
2302
- devices[tempDevice.serial_number] = tempDevice; // Store processed device
2294
+ isNaN(this.config.options?.maxStreams) === false
2295
+ ? Number(this.config.options.maxStreams)
2296
+ : this.deviceData.hksv === true
2297
+ ? 1
2298
+ : 2;
2299
+
2300
+ devices[tempDevice.serialNumber] = tempDevice; // Store processed device
2303
2301
  }
2304
2302
  });
2305
2303
 
@@ -2309,15 +2307,8 @@ export default class NestAccfactory {
2309
2307
  let processed = {};
2310
2308
  try {
2311
2309
  // Fix up data we need to
2312
-
2313
- // For the serial number, use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off structure for last 6 digits.
2314
- data.serial_number = '18B430' + crc24(object_key).toUpperCase();
2315
- data.excluded = this.config?.options?.weather === false; // Mark device as excluded or not
2310
+ data = process_common_data(object_key, data);
2316
2311
  data.device_type = NestAccfactory.DeviceType.WEATHER;
2317
- data.uuid = object_key; // Internal structure ID
2318
- data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
2319
- data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
2320
- data.description = typeof data?.description === 'string' ? makeHomeKitName(data.description) : '';
2321
2312
  data.model = 'Weather';
2322
2313
  data.current_temperature = data.weather.current_temperature;
2323
2314
  data.current_humidity = data.weather.current_humidity;
@@ -2328,41 +2319,12 @@ export default class NestAccfactory {
2328
2319
  data.sunset = data.weather.sunset;
2329
2320
  data.station = data.weather.station;
2330
2321
  data.forecast = data.weather.forecast;
2331
- data.elevation = 0;
2332
-
2333
- // Either use global elevation setting or one specific for device
2334
- if (typeof this.config?.devices?.[data.serial_number]?.elevation === 'number') {
2335
- data.elevation = this.config.devices[data.serial_number].elevation;
2336
- }
2337
-
2338
- if (data.elevation === 0 && typeof this.config?.options?.elevation === 'number') {
2339
- // Elevation from configuration
2340
- data.elevation = this.config.options.elevation;
2341
- }
2342
-
2343
- // Insert details for when using HAP-NodeJS library rather than Homebridge
2344
- if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
2345
- data.hkPairingCode = this.config.options.hkPairingCode;
2346
- }
2347
- if (
2348
- typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
2349
- this.config.devices[data.serial_number].hkPairingCode !== ''
2350
- ) {
2351
- data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
2352
- }
2353
- if (data?.hkPairingCode !== undefined) {
2354
- // Use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off serial number for last 6 digits.
2355
- let tempMACAddress = '18B430' + crc24(object_key).toUpperCase();
2356
- data.hkUsername = tempMACAddress
2357
- .toString('hex')
2358
- .split(/(..)/)
2359
- .filter((s) => s)
2360
- .join(':')
2361
- .toUpperCase(); // Create mac_address in format of xx:xx:xx:xx:xx:xx
2362
- }
2363
-
2364
- delete data.weather; // Don't need the 'weather' object in our output
2365
-
2322
+ data.elevation =
2323
+ isNaN(this.config?.devices?.[data?.serial_number]?.elevation) === false
2324
+ ? Number(this.config.devices[data.serial_number].elevation)
2325
+ : isNaN(this.config?.options?.elevation) === false
2326
+ ? Number(this.config.options.elevation)
2327
+ : 0;
2366
2328
  processed = data;
2367
2329
  // eslint-disable-next-line no-unused-vars
2368
2330
  } catch (error) {
@@ -2374,13 +2336,20 @@ export default class NestAccfactory {
2374
2336
  Object.entries(this.#rawData)
2375
2337
  .filter(
2376
2338
  ([key]) =>
2377
- (key.startsWith('structure.') === true || key.startsWith('STRUCTURE_') === true) && (deviceUUID === '' || deviceUUID === key),
2339
+ (key.startsWith('structure.') === true || key.startsWith('STRUCTURE_') === true) &&
2340
+ (deviceUUID === '' || deviceUUID === key) &&
2341
+ this.config?.options?.weather === true, // Only if weather enabled
2378
2342
  )
2379
2343
  .forEach(([object_key, value]) => {
2380
2344
  let tempDevice = {};
2381
2345
  try {
2382
- if (value?.source === NestAccfactory.DataSource.PROTOBUF) {
2346
+ if (
2347
+ value?.source === NestAccfactory.DataSource.PROTOBUF &&
2348
+ value.value?.structure_location?.geoCoordinate?.latitude !== undefined &&
2349
+ value.value?.structure_location?.geoCoordinate?.longitude !== undefined
2350
+ ) {
2383
2351
  let RESTTypeData = {};
2352
+ RESTTypeData.serialNumber = '18B430' + crc24(value.value.structure_info.rtsStructureId.toUpperCase()).toUpperCase();
2384
2353
  RESTTypeData.postal_code = value.value.structure_location.postalCode.value;
2385
2354
  RESTTypeData.country_code = value.value.structure_location.countryCode.value;
2386
2355
  RESTTypeData.city = value.value?.structure_location?.city !== undefined ? value.value.structure_location.city.value : '';
@@ -2393,12 +2362,18 @@ export default class NestAccfactory {
2393
2362
  : value.value.structure_info.name;
2394
2363
  RESTTypeData.weather = value.value.weather;
2395
2364
 
2396
- // Use the REST API structure ID from the Protobuf structure. This should prevent two 'weather' objects being created
2397
- let tempDevice = process_structure_data(value.value.structure_info.rtsStructureId, RESTTypeData);
2398
- tempDevice.uuid = object_key; // Use the Protobuf structure ID post processing
2365
+ // Use the REST API structure ID from the Protobuf structure. This will ensure we generate the same serial number
2366
+ // This should prevent two 'weather' objects being created
2367
+ tempDevice = process_structure_data(object_key, RESTTypeData);
2399
2368
  }
2400
- if (value?.source === NestAccfactory.DataSource.REST) {
2369
+
2370
+ if (
2371
+ value?.source === NestAccfactory.DataSource.REST &&
2372
+ value.value?.latitude !== undefined &&
2373
+ value.value?.longitude !== undefined
2374
+ ) {
2401
2375
  let RESTTypeData = {};
2376
+ RESTTypeData.serialNumber = '18B430' + crc24(object_key.toUpperCase()).toUpperCase();
2402
2377
  RESTTypeData.postal_code = value.value.postal_code;
2403
2378
  RESTTypeData.country_code = value.value.country_code;
2404
2379
  RESTTypeData.city = value.value?.city !== undefined ? value.value.city : '';
@@ -2415,35 +2390,37 @@ export default class NestAccfactory {
2415
2390
  this?.log?.debug && this.log.debug('Error processing data for weather');
2416
2391
  }
2417
2392
 
2418
- if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
2393
+ if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
2419
2394
  // Insert any extra options we've read in from configuration file for this device
2420
2395
  tempDevice.eveHistory =
2421
- this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
2422
- devices[tempDevice.serial_number] = tempDevice; // Store processed device
2396
+ this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serialNumber]?.eveHistory === true;
2397
+ devices[tempDevice.serialNumber] = tempDevice; // Store processed device
2423
2398
  }
2424
2399
  });
2425
2400
 
2426
2401
  return devices; // Return our processed data
2427
2402
  }
2428
2403
 
2429
- async #set(deviceUUID, values) {
2404
+ async #set(uuid, values) {
2430
2405
  if (
2431
- typeof deviceUUID !== 'string' ||
2432
- typeof this.#rawData?.[deviceUUID] !== 'object' ||
2406
+ typeof uuid !== 'string' ||
2407
+ uuid === '' ||
2433
2408
  typeof values !== 'object' ||
2434
- typeof this.#connections[this.#rawData?.[deviceUUID]?.connection] !== 'object'
2409
+ values?.uuid === undefined ||
2410
+ typeof this.#rawData?.[values?.uuid] !== 'object' ||
2411
+ typeof this.#connections?.[this.#rawData?.[values?.uuid]?.connection] !== 'object'
2435
2412
  ) {
2436
2413
  return;
2437
2414
  }
2438
2415
 
2439
- if (
2440
- this.#connections[this.#rawData[deviceUUID].connection].protobufRoot !== null &&
2441
- this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.PROTOBUF
2442
- ) {
2416
+ let nest_google_uuid = values.uuid; // Nest/Google structure uuid for this get request
2417
+ let connectionUuid = this.#rawData[values.uuid].connection; // Associated connection uuid for the uuid
2418
+
2419
+ if (this.#protobufRoot !== null && this.#rawData?.[nest_google_uuid]?.source === NestAccfactory.DataSource.PROTOBUF) {
2443
2420
  let updatedTraits = [];
2444
2421
  let protobufElement = {
2445
2422
  traitRequest: {
2446
- resourceId: deviceUUID,
2423
+ resourceId: nest_google_uuid,
2447
2424
  traitLabel: '',
2448
2425
  requestId: crypto.randomUUID(),
2449
2426
  },
@@ -2454,444 +2431,508 @@ export default class NestAccfactory {
2454
2431
  };
2455
2432
 
2456
2433
  await Promise.all(
2457
- Object.entries(values).map(async ([key, value]) => {
2458
- // Reset elements at start of loop
2459
- protobufElement.traitRequest.traitLabel = '';
2460
- protobufElement.state.type_url = '';
2461
- protobufElement.state.value = {};
2462
-
2463
- if (
2464
- (key === 'hvac_mode' &&
2465
- typeof value === 'string' &&
2466
- (value.toUpperCase() === 'OFF' ||
2467
- value.toUpperCase() === 'COOL' ||
2468
- value.toUpperCase() === 'HEAT' ||
2469
- value.toUpperCase() === 'RANGE')) ||
2470
- (key === 'target_temperature' &&
2471
- this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode === 'ECO_MODE_INACTIVE' &&
2472
- typeof value === 'number') ||
2473
- (key === 'target_temperature_low' &&
2474
- this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode === 'ECO_MODE_INACTIVE' &&
2475
- typeof value === 'number') ||
2476
- (key === 'target_temperature_high' &&
2477
- this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode === 'ECO_MODE_INACTIVE' &&
2478
- typeof value === 'number')
2479
- ) {
2480
- // Set either the 'mode' and/or non-eco temperatures on the target thermostat
2481
- protobufElement.traitRequest.traitLabel = 'target_temperature_settings';
2482
- protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.TargetTemperatureSettingsTrait';
2483
- protobufElement.state.value = this.#rawData[deviceUUID].value.target_temperature_settings;
2434
+ Object.entries(values)
2435
+ .filter(([key]) => key !== 'uuid')
2436
+ .map(async ([key, value]) => {
2437
+ // Reset elements at start of loop
2438
+ protobufElement.traitRequest.traitLabel = '';
2439
+ protobufElement.state.type_url = '';
2440
+ protobufElement.state.value = {};
2484
2441
 
2485
2442
  if (
2486
- (key === 'target_temperature_low' || key === 'target_temperature') &&
2487
- (protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_HEAT' ||
2488
- protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_RANGE')
2443
+ (key === 'hvac_mode' &&
2444
+ typeof value === 'string' &&
2445
+ (value.toUpperCase() === 'OFF' ||
2446
+ value.toUpperCase() === 'COOL' ||
2447
+ value.toUpperCase() === 'HEAT' ||
2448
+ value.toUpperCase() === 'RANGE')) ||
2449
+ (key === 'target_temperature' &&
2450
+ this.#rawData?.[nest_google_uuid]?.value?.eco_mode_state?.ecoMode === 'ECO_MODE_INACTIVE' &&
2451
+ isNaN(value) === false) ||
2452
+ (key === 'target_temperature_low' &&
2453
+ this.#rawData?.[nest_google_uuid]?.value?.eco_mode_state?.ecoMode === 'ECO_MODE_INACTIVE' &&
2454
+ isNaN(value) === false) ||
2455
+ (key === 'target_temperature_high' &&
2456
+ this.#rawData?.[nest_google_uuid]?.value?.eco_mode_state?.ecoMode === 'ECO_MODE_INACTIVE' &&
2457
+ isNaN(value) === false)
2489
2458
  ) {
2490
- // Changing heating target temperature
2491
- protobufElement.state.value.targetTemperature.heatingTarget = { value: value };
2459
+ // Set either the 'mode' and/or non-eco temperatures on the target thermostat
2460
+ protobufElement.traitRequest.traitLabel = 'target_temperature_settings';
2461
+ protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.TargetTemperatureSettingsTrait';
2462
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.target_temperature_settings;
2463
+
2464
+ if (
2465
+ (key === 'target_temperature_low' || key === 'target_temperature') &&
2466
+ (protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_HEAT' ||
2467
+ protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_RANGE')
2468
+ ) {
2469
+ // Changing heating target temperature
2470
+ protobufElement.state.value.targetTemperature.heatingTarget = { value: value };
2471
+ }
2472
+ if (
2473
+ (key === 'target_temperature_high' || key === 'target_temperature') &&
2474
+ (protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_COOL' ||
2475
+ protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_RANGE')
2476
+ ) {
2477
+ // Changing cooling target temperature
2478
+ protobufElement.state.value.targetTemperature.coolingTarget = { value: value };
2479
+ }
2480
+
2481
+ if (key === 'hvac_mode' && value.toUpperCase() !== 'OFF') {
2482
+ protobufElement.state.value.targetTemperature.setpointType = 'SET_POINT_TYPE_' + value.toUpperCase();
2483
+ protobufElement.state.value.enabled = { value: true };
2484
+ }
2485
+
2486
+ if (key === 'hvac_mode' && value.toUpperCase() === 'OFF') {
2487
+ protobufElement.state.value.enabled = { value: false };
2488
+ }
2489
+
2490
+ // Tag 'who is doing the temperature/mode change. We are :-)
2491
+ protobufElement.state.value.targetTemperature.currentActorInfo = {
2492
+ method: 'HVAC_ACTOR_METHOD_IOS',
2493
+ originator: this.#rawData[nest_google_uuid].value.target_temperature_settings.targetTemperature.currentActorInfo.originator,
2494
+ timeOfAction: { seconds: Math.floor(Date.now() / 1000), nanos: (Date.now() % 1000) * 1e6 },
2495
+ originatorRtsId: '',
2496
+ };
2492
2497
  }
2498
+
2493
2499
  if (
2494
- (key === 'target_temperature_high' || key === 'target_temperature') &&
2495
- (protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_COOL' ||
2496
- protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_RANGE')
2500
+ (key === 'target_temperature' &&
2501
+ this.#rawData?.[nest_google_uuid]?.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE' &&
2502
+ isNaN(value) === false) ||
2503
+ (key === 'target_temperature_low' &&
2504
+ this.#rawData?.[nest_google_uuid]?.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE' &&
2505
+ isNaN(value) === false) ||
2506
+ (key === 'target_temperature_high' &&
2507
+ this.#rawData?.[nest_google_uuid]?.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE' &&
2508
+ isNaN(value) === false)
2497
2509
  ) {
2498
- // Changing cooling target temperature
2499
- protobufElement.state.value.targetTemperature.coolingTarget = { value: value };
2510
+ // Set eco mode temperatures on the target thermostat
2511
+ protobufElement.traitRequest.traitLabel = 'eco_mode_settings';
2512
+ protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.EcoModeSettingsTrait';
2513
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.eco_mode_settings;
2514
+
2515
+ protobufElement.state.value.ecoTemperatureHeat.value.value =
2516
+ protobufElement.state.value.ecoTemperatureHeat.enabled === true &&
2517
+ protobufElement.state.value.ecoTemperatureCool.enabled === false
2518
+ ? value
2519
+ : protobufElement.state.value.ecoTemperatureHeat.value.value;
2520
+ protobufElement.state.value.ecoTemperatureCool.value.value =
2521
+ protobufElement.state.value.ecoTemperatureHeat.enabled === false &&
2522
+ protobufElement.state.value.ecoTemperatureCool.enabled === true
2523
+ ? value
2524
+ : protobufElement.state.value.ecoTemperatureCool.value.value;
2525
+ protobufElement.state.value.ecoTemperatureHeat.value.value =
2526
+ protobufElement.state.value.ecoTemperatureHeat.enabled === true &&
2527
+ protobufElement.state.value.ecoTemperatureCool.enabled === true &&
2528
+ key === 'target_temperature_low'
2529
+ ? value
2530
+ : protobufElement.state.value.ecoTemperatureHeat.value.value;
2531
+ protobufElement.state.value.ecoTemperatureCool.value.value =
2532
+ protobufElement.state.value.ecoTemperatureHeat.enabled === true &&
2533
+ protobufElement.state.value.ecoTemperatureCool.enabled === true &&
2534
+ key === 'target_temperature_high'
2535
+ ? value
2536
+ : protobufElement.state.value.ecoTemperatureCool.value.value;
2500
2537
  }
2501
2538
 
2502
- if (key === 'hvac_mode' && value.toUpperCase() !== 'OFF') {
2503
- protobufElement.state.value.targetTemperature.setpointType = 'SET_POINT_TYPE_' + value.toUpperCase();
2504
- protobufElement.state.value.enabled = { value: true };
2539
+ if (key === 'temperature_scale' && typeof value === 'string' && (value.toUpperCase() === 'C' || value.toUpperCase() === 'F')) {
2540
+ // Set the temperature scale on the target thermostat
2541
+ protobufElement.traitRequest.traitLabel = 'display_settings';
2542
+ protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.DisplaySettingsTrait';
2543
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.display_settings;
2544
+ protobufElement.state.value.temperatureScale = value.toUpperCase() === 'F' ? 'TEMPERATURE_SCALE_F' : 'TEMPERATURE_SCALE_C';
2505
2545
  }
2506
2546
 
2507
- if (key === 'hvac_mode' && value.toUpperCase() === 'OFF') {
2508
- protobufElement.state.value.enabled = { value: false };
2547
+ if (key === 'temperature_lock' && typeof value === 'boolean') {
2548
+ // Set lock mode on the target thermostat
2549
+ protobufElement.traitRequest.traitLabel = 'temperature_lock_settings';
2550
+ protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.TemperatureLockSettingsTrait';
2551
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.temperature_lock_settings;
2552
+ protobufElement.state.value.enabled = value === true;
2509
2553
  }
2510
2554
 
2511
- // Tage 'who is doing the temperature/mode change. We are :-)
2512
- protobufElement.state.value.targetTemperature.currentActorInfo = {
2513
- method: 'HVAC_ACTOR_METHOD_IOS',
2514
- originator: {
2515
- resourceId: Object.keys(this.#rawData)
2516
- .filter((key) => key.includes('USER_'))
2517
- .toString(),
2518
- },
2519
- timeOfAction: { seconds: Math.floor(Date.now() / 1000), nanos: (Date.now() % 1000) * 1e6 },
2520
- originatorRtsId: '',
2521
- };
2522
- }
2523
-
2524
- if (
2525
- (key === 'target_temperature' &&
2526
- this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE' &&
2527
- typeof value === 'number') ||
2528
- (key === 'target_temperature_low' &&
2529
- this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE' &&
2530
- typeof value === 'number') ||
2531
- (key === 'target_temperature_high' &&
2532
- this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE' &&
2533
- typeof value === 'number')
2534
- ) {
2535
- // Set eco mode temperatures on the target thermostat
2536
- protobufElement.traitRequest.traitLabel = 'eco_mode_settings';
2537
- protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.EcoModeSettingsTrait';
2538
- protobufElement.state.value = this.#rawData[deviceUUID].value.eco_mode_settings;
2539
-
2540
- protobufElement.state.value.ecoTemperatureHeat.value.value =
2541
- protobufElement.state.value.ecoTemperatureHeat.enabled === true &&
2542
- protobufElement.state.value.ecoTemperatureCool.enabled === false
2543
- ? value
2544
- : protobufElement.state.value.ecoTemperatureHeat.value.value;
2545
- protobufElement.state.value.ecoTemperatureCool.value.value =
2546
- protobufElement.state.value.ecoTemperatureHeat.enabled === false &&
2547
- protobufElement.state.value.ecoTemperatureCool.enabled === true
2548
- ? value
2549
- : protobufElement.state.value.ecoTemperatureCool.value.value;
2550
- protobufElement.state.value.ecoTemperatureHeat.value.value =
2551
- protobufElement.state.value.ecoTemperatureHeat.enabled === true &&
2552
- protobufElement.state.value.ecoTemperatureCool.enabled === true &&
2553
- key === 'target_temperature_low'
2554
- ? value
2555
- : protobufElement.state.value.ecoTemperatureHeat.value.value;
2556
- protobufElement.state.value.ecoTemperatureCool.value.value =
2557
- protobufElement.state.value.ecoTemperatureHeat.enabled === true &&
2558
- protobufElement.state.value.ecoTemperatureCool.enabled === true &&
2559
- key === 'target_temperature_high'
2560
- ? value
2561
- : protobufElement.state.value.ecoTemperatureCool.value.value;
2562
- }
2563
-
2564
- if (key === 'temperature_scale' && typeof value === 'string' && (value.toUpperCase() === 'C' || value.toUpperCase() === 'F')) {
2565
- // Set the temperature scale on the target thermostat
2566
- protobufElement.traitRequest.traitLabel = 'display_settings';
2567
- protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.DisplaySettingsTrait';
2568
- protobufElement.state.value = this.#rawData[deviceUUID].value.display_settings;
2569
- protobufElement.state.value.temperatureScale = value.toUpperCase() === 'F' ? 'TEMPERATURE_SCALE_F' : 'TEMPERATURE_SCALE_C';
2570
- }
2555
+ if (key === 'fan_state' && typeof value === 'boolean') {
2556
+ // Set fan mode on the target thermostat
2557
+ protobufElement.traitRequest.traitLabel = 'fan_control_settings';
2558
+ protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.FanControlSettingsTrait';
2559
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.fan_control_settings;
2560
+ protobufElement.state.value.timerEnd =
2561
+ value === true
2562
+ ? {
2563
+ seconds: Number(Math.floor(Date.now() / 1000 + Number(protobufElement.state.value.timerDuration.seconds))),
2564
+ nanos: (Math.floor(Date.now() / 1000 + Number(protobufElement.state.value.timerDuration.seconds)) % 1000) * 1e6,
2565
+ }
2566
+ : { seconds: 0, nanos: 0 };
2567
+ if (values?.fan_timer_speed !== undefined) {
2568
+ // We have a value to set fan speed also, so handle here as combined setting
2569
+ protobufElement.state.value.timerSpeed =
2570
+ values?.fan_timer_speed !== 0
2571
+ ? 'FAN_SPEED_SETTING_STAGE' + values?.fan_timer_speed
2572
+ : this.#rawData[nest_google_uuid].value.fan_control_settings.timerSpeed;
2573
+ }
2574
+ }
2571
2575
 
2572
- if (key === 'temperature_lock' && typeof value === 'boolean') {
2573
- // Set lock mode on the target thermostat
2574
- protobufElement.traitRequest.traitLabel = 'temperature_lock_settings';
2575
- protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.TemperatureLockSettingsTrait';
2576
- protobufElement.state.value = this.#rawData[deviceUUID].value.temperature_lock_settings;
2577
- protobufElement.state.value.enabled = value === true;
2578
- }
2576
+ if (key === 'fan_timer_speed' && isNaN(value) === false && values?.fan_state === undefined) {
2577
+ // Set fan speed on the target thermostat only if we're not changing fan on/off state also
2578
+ protobufElement.traitRequest.traitLabel = 'fan_control_settings';
2579
+ protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.FanControlSettingsTrait';
2580
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.fan_control_settings;
2581
+ protobufElement.state.value.timerSpeed =
2582
+ value !== 0 ? 'FAN_SPEED_SETTING_STAGE' + value : this.#rawData[nest_google_uuid].value.fan_control_settings.timerSpeed;
2583
+ }
2579
2584
 
2580
- if (key === 'fan_state' && typeof value === 'boolean') {
2581
- // Set fan mode on the target thermostat
2582
- let endTime =
2583
- value === true
2584
- ? Math.floor(Date.now() / 1000) + this.#rawData[deviceUUID].value.fan_control_settings.timerDuration.seconds
2585
- : 0;
2585
+ if (key === 'statusled_brightness' && isNaN(value) === false) {
2586
+ // 0
2587
+ // 1
2588
+ }
2586
2589
 
2587
- protobufElement.traitRequest.traitLabel = 'fan_control_settings';
2588
- protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.FanControlSettingsTrait';
2589
- protobufElement.state.value = this.#rawData[deviceUUID].value.fan_control_settings;
2590
- protobufElement.state.value.timerEnd = { seconds: endTime, nanos: (endTime % 1000) * 1e6 };
2591
- }
2590
+ if (key === 'irled_enabled' && typeof value === 'string') {
2591
+ // 'auto_on'
2592
+ // 'always_off'
2593
+ }
2592
2594
 
2593
- //if (key === 'statusled.brightness'
2594
- //if (key === 'irled.state'
2595
-
2596
- if (key === 'streaming.enabled' && typeof value === 'boolean') {
2597
- // Turn camera video on/off
2598
- protobufElement.traitRequest.traitLabel = 'recording_toggle_settings';
2599
- protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.product.camera.RecordingToggleSettingsTrait';
2600
- protobufElement.state.value = this.#rawData[deviceUUID].value.recording_toggle_settings;
2601
- protobufElement.state.value.targetCameraState = value === true ? 'CAMERA_ON' : 'CAMERA_OFF';
2602
- protobufElement.state.value.changeModeReason = 2;
2603
- protobufElement.state.value.settingsUpdated = { seconds: Math.floor(Date.now() / 1000), nanos: (Date.now() % 1000) * 1e6 };
2604
- }
2595
+ if (key === 'streaming_enabled' && typeof value === 'boolean') {
2596
+ // Turn camera video on/off
2597
+ protobufElement.traitRequest.traitLabel = 'recording_toggle_settings';
2598
+ protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.product.camera.RecordingToggleSettingsTrait';
2599
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.recording_toggle_settings;
2600
+ protobufElement.state.value.targetCameraState = value === true ? 'CAMERA_ON' : 'CAMERA_OFF';
2601
+ protobufElement.state.value.changeModeReason = 2;
2602
+ protobufElement.state.value.settingsUpdated = { seconds: Math.floor(Date.now() / 1000), nanos: (Date.now() % 1000) * 1e6 };
2603
+ }
2605
2604
 
2606
- if (key === 'audio.enabled' && typeof value === 'boolean') {
2607
- // Enable/disable microphone on camera/doorbell
2608
- protobufElement.traitRequest.traitLabel = 'microphone_settings';
2609
- protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.audio.MicrophoneSettingsTrait';
2610
- protobufElement.state.value = this.#rawData[deviceUUID].value.microphone_settings;
2611
- protobufElement.state.value.enableMicrophone = value;
2612
- }
2605
+ if (key === 'audio_enabled' && typeof value === 'boolean') {
2606
+ // Enable/disable microphone on camera/doorbell
2607
+ protobufElement.traitRequest.traitLabel = 'microphone_settings';
2608
+ protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.audio.MicrophoneSettingsTrait';
2609
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.microphone_settings;
2610
+ protobufElement.state.value.enableMicrophone = value;
2611
+ }
2613
2612
 
2614
- if (key === 'indoor_chime_enabled' && typeof value === 'boolean') {
2615
- // Enable/disable chime status on doorbell
2616
- protobufElement.traitRequest.traitLabel = 'doorbell_indoor_chime_settings';
2617
- protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.product.doorbell.DoorbellIndoorChimeSettingsTrait';
2618
- protobufElement.state.value = this.#rawData[deviceUUID].value.doorbell_indoor_chime_settings;
2619
- protobufElement.state.value.chimeEnabled = value;
2620
- }
2613
+ if (key === 'indoor_chime_enabled' && typeof value === 'boolean') {
2614
+ // Enable/disable chime status on doorbell
2615
+ protobufElement.traitRequest.traitLabel = 'doorbell_indoor_chime_settings';
2616
+ protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.product.doorbell.DoorbellIndoorChimeSettingsTrait';
2617
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.doorbell_indoor_chime_settings;
2618
+ protobufElement.state.value.chimeEnabled = value;
2619
+ }
2621
2620
 
2622
- if (key === 'light_enabled' && typeof value === 'boolean') {
2623
- // Turn on/off light on supported camera devices. Need to find the related or SERVICE__ object for teh device
2624
- let serviceUUID = undefined;
2625
- if (this.#rawData[deviceUUID].value?.related_resources?.relatedResources !== undefined) {
2626
- Object.values(this.#rawData[deviceUUID].value?.related_resources?.relatedResources).forEach((values) => {
2627
- if (
2628
- values?.resourceTypeName?.resourceName === 'google.resource.AzizResource' &&
2629
- values?.resourceId?.resourceId.startsWith('SERVICE_') === true
2630
- ) {
2631
- serviceUUID = values.resourceId.resourceId;
2632
- }
2633
- });
2621
+ if (key === 'light_enabled' && typeof value === 'boolean') {
2622
+ // Turn on/off light on supported camera devices. Need to find the related or 'SERVICE_' object for the device
2623
+ let serviceUUID = undefined;
2624
+ if (this.#rawData[nest_google_uuid].value?.related_resources?.relatedResources !== undefined) {
2625
+ Object.values(this.#rawData[nest_google_uuid].value?.related_resources?.relatedResources).forEach((values) => {
2626
+ if (
2627
+ values?.resourceTypeName?.resourceName === 'google.resource.AzizResource' &&
2628
+ values?.resourceId?.resourceId.startsWith('SERVICE_') === true
2629
+ ) {
2630
+ serviceUUID = values.resourceId.resourceId;
2631
+ }
2632
+ });
2634
2633
 
2635
- if (serviceUUID !== undefined) {
2636
- let commandResponse = await this.#protobufCommand(this.#rawData[deviceUUID].connection, 'ResourceApi', 'SendCommand', {
2637
- resourceRequest: {
2638
- resourceId: serviceUUID,
2639
- requestId: crypto.randomUUID(),
2640
- },
2641
- resourceCommands: [
2642
- {
2643
- traitLabel: 'on_off',
2644
- command: {
2645
- type_url: 'type.nestlabs.com/weave.trait.actuator.OnOffTrait.SetStateRequest',
2646
- value: {
2647
- on: value,
2634
+ if (serviceUUID !== undefined) {
2635
+ let commandResponse = await this.#protobufCommand(connectionUuid, 'ResourceApi', 'SendCommand', {
2636
+ resourceRequest: {
2637
+ resourceId: serviceUUID,
2638
+ requestId: crypto.randomUUID(),
2639
+ },
2640
+ resourceCommands: [
2641
+ {
2642
+ traitLabel: 'on_off',
2643
+ command: {
2644
+ type_url: 'type.nestlabs.com/weave.trait.actuator.OnOffTrait.SetStateRequest',
2645
+ value: {
2646
+ on: value,
2647
+ },
2648
2648
  },
2649
2649
  },
2650
- },
2651
- ],
2652
- });
2650
+ ],
2651
+ });
2653
2652
 
2654
- if (commandResponse.sendCommandResponse?.[0]?.traitOperations?.[0]?.progress !== 'COMPLETE') {
2655
- this?.log?.debug && this.log.debug('Protobuf API had error setting light status on uuid "%s"', deviceUUID);
2653
+ if (commandResponse.sendCommandResponse?.[0]?.traitOperations?.[0]?.progress !== 'COMPLETE') {
2654
+ this?.log?.debug && this.log.debug('Protobuf API had error setting light status on uuid "%s"', nest_google_uuid);
2655
+ }
2656
2656
  }
2657
2657
  }
2658
2658
  }
2659
- }
2660
2659
 
2661
- if (key === 'light_brightness' && typeof value === 'number') {
2662
- // Set light brightness on supported camera devices
2663
- protobufElement.traitRequest.traitLabel = 'floodlight_settings';
2664
- protobufElement.state.type_url = 'type.nestlabs.com/google.trait.product.camera.FloodlightSettingsTrait';
2665
- protobufElement.state.value = this.#rawData[deviceUUID].value.floodlight_settings;
2666
- protobufElement.state.value.brightness = scaleValue(value, 0, 100, 0, 10); // Scale to required level
2667
- }
2660
+ if (key === 'light_brightness' && isNaN(value) === false) {
2661
+ // Set light brightness on supported camera devices
2662
+ protobufElement.traitRequest.traitLabel = 'floodlight_settings';
2663
+ protobufElement.state.type_url = 'type.nestlabs.com/google.trait.product.camera.FloodlightSettingsTrait';
2664
+ protobufElement.state.value = this.#rawData[nest_google_uuid].value.floodlight_settings;
2665
+ protobufElement.state.value.brightness = scaleValue(Number(value), 0, 100, 0, 10); // Scale to required level
2666
+ }
2668
2667
 
2669
- if (protobufElement.traitRequest.traitLabel === '' || protobufElement.state.type_url === '') {
2670
- this?.log?.debug && this.log.debug('Unknown Protobuf set key "%s" for device uuid "%s"', key, deviceUUID);
2671
- }
2668
+ if (protobufElement.traitRequest.traitLabel === '' || protobufElement.state.type_url === '') {
2669
+ this?.log?.debug && this.log.debug('Unknown Protobuf set key "%s" for device uuid "%s"', key, nest_google_uuid);
2670
+ }
2672
2671
 
2673
- if (protobufElement.traitRequest.traitLabel !== '' && protobufElement.state.type_url !== '') {
2674
- // eslint-disable-next-line no-undef
2675
- updatedTraits.push(structuredClone(protobufElement));
2676
- }
2677
- }),
2672
+ if (protobufElement.traitRequest.traitLabel !== '' && protobufElement.state.type_url !== '') {
2673
+ // eslint-disable-next-line no-undef
2674
+ updatedTraits.push(structuredClone(protobufElement));
2675
+ }
2676
+ }),
2678
2677
  );
2679
2678
 
2680
2679
  if (updatedTraits.length !== 0) {
2681
- let commandResponse = await this.#protobufCommand(this.#rawData[deviceUUID].connection, 'TraitBatchApi', 'BatchUpdateState', {
2680
+ let commandResponse = await this.#protobufCommand(connectionUuid, 'TraitBatchApi', 'BatchUpdateState', {
2682
2681
  batchUpdateStateRequest: updatedTraits,
2683
2682
  });
2684
2683
  if (
2685
2684
  commandResponse === undefined ||
2686
2685
  commandResponse?.batchUpdateStateResponse?.[0]?.traitOperations?.[0]?.progress !== 'COMPLETE'
2687
2686
  ) {
2688
- this?.log?.debug && this.log.debug('Protobuf API had error updating device traits for uuid "%s"', deviceUUID);
2687
+ this?.log?.debug && this.log.debug('Protobuf API had error updating device traits for uuid "%s"', nest_google_uuid);
2689
2688
  }
2690
2689
  }
2691
2690
  }
2692
2691
 
2693
- if (this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.REST && deviceUUID.startsWith('quartz.') === true) {
2692
+ if (this.#rawData?.[nest_google_uuid]?.source === NestAccfactory.DataSource.REST && nest_google_uuid.startsWith('quartz.') === true) {
2694
2693
  // Set value on Nest Camera/Doorbell
2695
2694
  await Promise.all(
2696
- Object.entries(values).map(async ([key, value]) => {
2697
- const SETPROPERTIES = {
2698
- indoor_chime_enabled: 'doorbell.indoor_chime.enabled',
2699
- statusled_brightness: 'statusled.brightness',
2700
- irled_enabled: 'irled.state',
2701
- streaming_enabled: 'streaming.enabled',
2702
- audio_enabled: 'audio.enabled',
2703
- };
2695
+ Object.entries(values)
2696
+ .filter(([key]) => key !== 'uuid')
2697
+ .map(async ([key, value]) => {
2698
+ const SETPROPERTIES = {
2699
+ indoor_chime_enabled: 'doorbell.indoor_chime.enabled',
2700
+ statusled_brightness: 'statusled.brightness',
2701
+ irled_enabled: 'irled.state',
2702
+ streaming_enabled: 'streaming.enabled',
2703
+ audio_enabled: 'audio.enabled',
2704
+ };
2704
2705
 
2705
- // Transform key to correct set camera properties key
2706
- key = SETPROPERTIES[key] !== undefined ? SETPROPERTIES[key] : key;
2706
+ // Transform key to correct set camera properties key
2707
+ key = SETPROPERTIES[key] !== undefined ? SETPROPERTIES[key] : key;
2707
2708
 
2708
- await fetchWrapper(
2709
- 'post',
2710
- 'https://webapi.' + this.#connections[this.#rawData[deviceUUID].connection].cameraAPIHost + '/api/dropcams.set_properties',
2711
- {
2712
- headers: {
2713
- referer: 'https://' + this.#connections[this.#rawData[deviceUUID].connection].referer,
2714
- 'User-Agent': USERAGENT,
2715
- 'Content-Type': 'application/x-www-form-urlencoded',
2716
- [this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.key]:
2717
- this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.value +
2718
- this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.token,
2709
+ await fetchWrapper(
2710
+ 'post',
2711
+ 'https://webapi.' + this.#connections[connectionUuid].cameraAPIHost + '/api/dropcams.set_properties',
2712
+ {
2713
+ headers: {
2714
+ referer: 'https://' + this.#connections[connectionUuid].referer,
2715
+ 'User-Agent': USERAGENT,
2716
+ 'Content-Type': 'application/x-www-form-urlencoded',
2717
+ [this.#connections[connectionUuid].cameraAPI.key]:
2718
+ this.#connections[connectionUuid].cameraAPI.value + this.#connections[connectionUuid].cameraAPI.token,
2719
+ },
2720
+ timeout: NESTAPITIMEOUT,
2719
2721
  },
2720
- timeout: NESTAPITIMEOUT,
2721
- },
2722
- [key] + '=' + value + '&uuid=' + deviceUUID.split('.')[1],
2723
- )
2724
- .then((response) => response.json())
2725
- .then((data) => {
2726
- if (data?.status !== 0) {
2727
- throw new Error('REST API camera update for failed with error');
2728
- }
2729
- })
2730
- .catch((error) => {
2731
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
2732
- this.log.debug('REST API camera update for failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
2733
- }
2734
- });
2735
- }),
2722
+ [key] + '=' + value + '&uuid=' + nest_google_uuid.split('.')[1],
2723
+ )
2724
+ .then((response) => response.json())
2725
+ .then((data) => {
2726
+ if (data?.status !== 0) {
2727
+ throw new Error('REST API camera update for failed with error');
2728
+ }
2729
+ })
2730
+ .catch((error) => {
2731
+ if (
2732
+ error?.cause !== undefined &&
2733
+ JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
2734
+ this?.log?.debug
2735
+ ) {
2736
+ this.log.debug(
2737
+ 'REST API camera update for failed with error for uuid "%s". Error was "%s"',
2738
+ nest_google_uuid,
2739
+ error?.code,
2740
+ );
2741
+ }
2742
+ });
2743
+ }),
2736
2744
  );
2737
2745
  }
2738
2746
 
2739
- if (this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.REST && deviceUUID.startsWith('quartz.') === false) {
2747
+ if (this.#rawData?.[nest_google_uuid]?.source === NestAccfactory.DataSource.REST && nest_google_uuid.startsWith('quartz.') === false) {
2740
2748
  // set values on other Nest devices besides cameras/doorbells
2741
2749
  await Promise.all(
2742
- Object.entries(values).map(async ([key, value]) => {
2743
- let subscribeJSONData = { objects: [] };
2750
+ Object.entries(values)
2751
+ .filter(([key]) => key !== 'uuid')
2752
+ .map(async ([key, value]) => {
2753
+ let subscribeJSONData = { objects: [] };
2744
2754
 
2745
- if (deviceUUID.startsWith('device.') === false) {
2746
- subscribeJSONData.objects.push({ object_key: deviceUUID, op: 'MERGE', value: { [key]: value } });
2747
- }
2755
+ if (nest_google_uuid.startsWith('device.') === false) {
2756
+ subscribeJSONData.objects.push({ object_key: nest_google_uuid, op: 'MERGE', value: { [key]: value } });
2757
+ }
2748
2758
 
2749
- // Some elements when setting thermostat data are located in a different object locations than with the device object
2750
- // Handle this scenario below
2751
- if (deviceUUID.startsWith('device.') === true) {
2752
- let RESTStructureUUID = deviceUUID;
2759
+ // Some elements when setting thermostat data are located in a different object locations than with the device object
2760
+ // Handle this scenario below
2761
+ if (nest_google_uuid.startsWith('device.') === true) {
2762
+ let RESTStructureUUID = nest_google_uuid;
2753
2763
 
2754
- if (
2755
- (key === 'hvac_mode' &&
2756
- typeof value === 'string' &&
2757
- (value.toUpperCase() === 'OFF' ||
2758
- value.toUpperCase() === 'COOL' ||
2759
- value.toUpperCase() === 'HEAT' ||
2760
- value.toUpperCase() === 'RANGE')) ||
2761
- (key === 'target_temperature' && typeof value === 'number') ||
2762
- (key === 'target_temperature_low' && typeof value === 'number') ||
2763
- (key === 'target_temperature_high' && typeof value === 'number')
2764
- ) {
2765
- RESTStructureUUID = 'shared.' + deviceUUID.split('.')[1];
2764
+ if (
2765
+ (key === 'hvac_mode' &&
2766
+ typeof value === 'string' &&
2767
+ (value.toUpperCase() === 'OFF' ||
2768
+ value.toUpperCase() === 'COOL' ||
2769
+ value.toUpperCase() === 'HEAT' ||
2770
+ value.toUpperCase() === 'RANGE')) ||
2771
+ (key === 'target_temperature' && isNaN(value) === false) ||
2772
+ (key === 'target_temperature_low' && isNaN(value) === false) ||
2773
+ (key === 'target_temperature_high' && isNaN(value) === false)
2774
+ ) {
2775
+ RESTStructureUUID = 'shared.' + nest_google_uuid.split('.')[1];
2776
+ }
2777
+
2778
+ if (key === 'fan_state' && typeof value === 'boolean') {
2779
+ key = 'fan_timer_timeout';
2780
+ value = value === true ? this.#rawData[nest_google_uuid].value.fan_duration + Math.floor(Date.now() / 1000) : 0;
2781
+ }
2782
+
2783
+ if (key === 'fan_timer_speed' && isNaN(value) === false) {
2784
+ value = value !== 0 ? 'stage' + value : 'stage1';
2785
+ }
2786
+
2787
+ subscribeJSONData.objects.push({ object_key: RESTStructureUUID, op: 'MERGE', value: { [key]: value } });
2766
2788
  }
2767
- subscribeJSONData.objects.push({ object_key: RESTStructureUUID, op: 'MERGE', value: { [key]: value } });
2768
- }
2769
2789
 
2770
- if (subscribeJSONData.objects.length !== 0) {
2771
- await fetchWrapper(
2772
- 'post',
2773
- this.#connections[this.#rawData[deviceUUID].connection].transport_url + '/v5/put',
2774
- {
2775
- referer: 'https://' + this.#connections[this.#rawData[deviceUUID].connection].referer,
2776
- headers: {
2777
- 'User-Agent': USERAGENT,
2778
- Authorization: 'Basic ' + this.#connections[this.#rawData[deviceUUID].connection].token,
2790
+ if (subscribeJSONData.objects.length !== 0) {
2791
+ await fetchWrapper(
2792
+ 'post',
2793
+ this.#connections[connectionUuid].transport_url + '/v5/put',
2794
+ {
2795
+ referer: 'https://' + this.#connections[connectionUuid].referer,
2796
+ headers: {
2797
+ 'User-Agent': USERAGENT,
2798
+ Authorization: 'Basic ' + this.#connections[connectionUuid].token,
2799
+ },
2779
2800
  },
2780
- },
2781
- JSON.stringify(subscribeJSONData),
2782
- ).catch((error) => {
2783
- this?.log?.debug &&
2784
- this.log.debug('REST API property update for failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
2785
- });
2786
- }
2787
- }),
2801
+ JSON.stringify(subscribeJSONData),
2802
+ ).catch((error) => {
2803
+ this?.log?.debug &&
2804
+ this.log.debug(
2805
+ 'REST API property update for failed with error for uuid "%s". Error was "%s"',
2806
+ nest_google_uuid,
2807
+ error?.code,
2808
+ );
2809
+ });
2810
+ }
2811
+ }),
2788
2812
  );
2789
2813
  }
2790
2814
  }
2791
2815
 
2792
- async #get(deviceUUID, values) {
2816
+ async #get(uuid, values) {
2793
2817
  if (
2794
- typeof deviceUUID !== 'string' ||
2795
- typeof this.#rawData?.[deviceUUID] !== 'object' ||
2818
+ typeof uuid !== 'string' ||
2819
+ uuid === '' ||
2796
2820
  typeof values !== 'object' ||
2797
- typeof this.#connections[this.#rawData?.[deviceUUID]?.connection] !== 'object'
2821
+ values?.uuid === undefined ||
2822
+ typeof this.#rawData?.[values?.uuid] !== 'object' ||
2823
+ typeof this.#connections?.[this.#rawData?.[values?.uuid]?.connection] !== 'object'
2798
2824
  ) {
2799
- values = {};
2825
+ return;
2800
2826
  }
2801
2827
 
2802
- await Promise.all(
2803
- Object.entries(values).map(async ([key]) => {
2804
- // We'll return the data under the original key value
2805
- // By default, the returned value will be undefined. If call is successful, the key value will have the data requested
2806
- values[key] = undefined;
2807
-
2808
- if (
2809
- this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.REST &&
2810
- key === 'camera_snapshot' &&
2811
- deviceUUID.startsWith('quartz.') === true &&
2812
- typeof this.#rawData?.[deviceUUID]?.value?.nexus_api_http_server_url === 'string' &&
2813
- this.#rawData[deviceUUID].value.nexus_api_http_server_url !== ''
2814
- ) {
2815
- // Attempt to retrieve snapshot from camera via REST API
2816
- await fetchWrapper(
2817
- 'get',
2818
- this.#rawData[deviceUUID].value.nexus_api_http_server_url + '/get_image?uuid=' + deviceUUID.split('.')[1],
2819
- {
2820
- headers: {
2821
- referer: 'https://' + this.#connections[this.#rawData[deviceUUID].connection].referer,
2822
- 'User-Agent': USERAGENT,
2823
- [this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.key]:
2824
- this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.value +
2825
- this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.token,
2826
- },
2827
- timeout: 3000,
2828
- },
2829
- )
2830
- .then((response) => response.arrayBuffer())
2831
- .then((data) => {
2832
- values[key] = Buffer.from(data);
2833
- })
2834
- .catch((error) => {
2835
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
2836
- this.log.debug('REST API camera snapshot failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
2837
- }
2838
- });
2839
- }
2828
+ let nest_google_uuid = values.uuid; // Nest/Google structure uuid for this get request
2829
+ let connectionUuid = this.#rawData[values.uuid].connection; // Associated connection uuid for the uuid
2840
2830
 
2841
- if (
2842
- this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.PROTOBUF &&
2843
- this.#connections[this.#rawData[deviceUUID].connection].protobufRoot !== null &&
2844
- this.#rawData[deviceUUID]?.value?.device_identity?.vendorProductId !== undefined &&
2845
- key === 'camera_snapshot'
2846
- ) {
2847
- // Attempt to retrieve snapshot from camera via Protobuf API
2848
- // First, request to get snapshot url image updated
2849
- let commandResponse = await this.#protobufCommand(this.#rawData[deviceUUID].connection, 'ResourceApi', 'SendCommand', {
2850
- resourceRequest: {
2851
- resourceId: deviceUUID,
2852
- requestId: crypto.randomUUID(),
2853
- },
2854
- resourceCommands: [
2855
- {
2856
- traitLabel: 'upload_live_image',
2857
- command: {
2858
- type_url: 'type.nestlabs.com/nest.trait.product.camera.UploadLiveImageTrait.UploadLiveImageRequest',
2859
- value: {},
2860
- },
2861
- },
2862
- ],
2863
- });
2831
+ await Promise.all(
2832
+ Object.entries(values)
2833
+ .filter(([key]) => key !== 'uuid')
2834
+ .map(async ([key]) => {
2835
+ // We'll return the data under the original key value
2836
+ // By default, the returned value will be undefined. If call is successful, the key value will have the data requested
2837
+ values[key] = undefined;
2864
2838
 
2865
2839
  if (
2866
- commandResponse?.sendCommandResponse?.[0]?.traitOperations?.[0]?.progress === 'COMPLETE' &&
2867
- typeof this.#rawData?.[deviceUUID]?.value?.upload_live_image?.liveImageUrl === 'string' &&
2868
- this.#rawData[deviceUUID].value.upload_live_image.liveImageUrl !== ''
2840
+ this.#rawData?.[nest_google_uuid]?.source === NestAccfactory.DataSource.REST &&
2841
+ key === 'camera_snapshot' &&
2842
+ nest_google_uuid.startsWith('quartz.') === true &&
2843
+ typeof this.#rawData?.[nest_google_uuid]?.value?.nexus_api_http_server_url === 'string' &&
2844
+ this.#rawData[nest_google_uuid].value.nexus_api_http_server_url !== ''
2869
2845
  ) {
2870
- // Snapshot url image has beeen updated, so no retrieve it
2871
- await fetchWrapper('get', this.#rawData[deviceUUID].value.upload_live_image.liveImageUrl, {
2872
- referer: 'https://' + this.#connections[this.#rawData[deviceUUID].connection].referer,
2873
- headers: {
2874
- 'User-Agent': USERAGENT,
2875
- Authorization: 'Basic ' + this.#connections[this.#rawData[deviceUUID].connection].token,
2846
+ // Attempt to retrieve snapshot from camera via REST API
2847
+ await fetchWrapper(
2848
+ 'get',
2849
+ this.#rawData[nest_google_uuid].value.nexus_api_http_server_url + '/get_image?uuid=' + nest_google_uuid.split('.')[1],
2850
+ {
2851
+ headers: {
2852
+ referer: 'https://' + this.#connections[connectionUuid].referer,
2853
+ 'User-Agent': USERAGENT,
2854
+ [this.#connections[connectionUuid].cameraAPI.key]:
2855
+ this.#connections[connectionUuid].cameraAPI.value + this.#connections[connectionUuid].cameraAPI.token,
2856
+ },
2857
+ timeout: 3000,
2876
2858
  },
2877
- timeout: 3000,
2878
- })
2859
+ )
2879
2860
  .then((response) => response.arrayBuffer())
2880
2861
  .then((data) => {
2881
2862
  values[key] = Buffer.from(data);
2882
2863
  })
2883
2864
  .catch((error) => {
2884
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
2885
- this.log.debug('Protobuf API camera snapshot failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
2865
+ if (
2866
+ error?.cause !== undefined &&
2867
+ JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
2868
+ this?.log?.debug
2869
+ ) {
2870
+ this.log.debug('REST API camera snapshot failed with error for uuid "%s". Error was "%s"', nest_google_uuid, error?.code);
2886
2871
  }
2887
2872
  });
2888
2873
  }
2889
- }
2890
- }),
2874
+
2875
+ if (
2876
+ this.#rawData?.[nest_google_uuid]?.source === NestAccfactory.DataSource.PROTOBUF &&
2877
+ this.#protobufRoot !== null &&
2878
+ this.#rawData[nest_google_uuid]?.value?.device_identity?.vendorProductId !== undefined &&
2879
+ key === 'camera_snapshot'
2880
+ ) {
2881
+ // Attempt to retrieve snapshot from camera via Protobuf API
2882
+ // First, request to get snapshot url image updated
2883
+ let commandResponse = await this.#protobufCommand(connectionUuid, 'ResourceApi', 'SendCommand', {
2884
+ resourceRequest: {
2885
+ resourceId: nest_google_uuid,
2886
+ requestId: crypto.randomUUID(),
2887
+ },
2888
+ resourceCommands: [
2889
+ {
2890
+ traitLabel: 'upload_live_image',
2891
+ command: {
2892
+ type_url: 'type.nestlabs.com/nest.trait.product.camera.UploadLiveImageTrait.UploadLiveImageRequest',
2893
+ value: {},
2894
+ },
2895
+ },
2896
+ ],
2897
+ });
2898
+
2899
+ if (
2900
+ commandResponse?.sendCommandResponse?.[0]?.traitOperations?.[0]?.progress === 'COMPLETE' &&
2901
+ typeof this.#rawData?.[nest_google_uuid]?.value?.upload_live_image?.liveImageUrl === 'string' &&
2902
+ this.#rawData[nest_google_uuid].value.upload_live_image.liveImageUrl !== ''
2903
+ ) {
2904
+ // Snapshot url image has beeen updated, so no retrieve it
2905
+ await fetchWrapper('get', this.#rawData[nest_google_uuid].value.upload_live_image.liveImageUrl, {
2906
+ referer: 'https://' + this.#connections[connectionUuid].referer,
2907
+ headers: {
2908
+ 'User-Agent': USERAGENT,
2909
+ Authorization: 'Basic ' + this.#connections[connectionUuid].token,
2910
+ },
2911
+ timeout: 3000,
2912
+ })
2913
+ .then((response) => response.arrayBuffer())
2914
+ .then((data) => {
2915
+ values[key] = Buffer.from(data);
2916
+ })
2917
+ .catch((error) => {
2918
+ if (
2919
+ error?.cause !== undefined &&
2920
+ JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
2921
+ this?.log?.debug
2922
+ ) {
2923
+ this.log.debug(
2924
+ 'Protobuf API camera snapshot failed with error for uuid "%s". Error was "%s"',
2925
+ nest_google_uuid,
2926
+ error?.code,
2927
+ );
2928
+ }
2929
+ });
2930
+ }
2931
+ }
2932
+ }),
2891
2933
  );
2892
2934
 
2893
- // Send results back via event
2894
- this.#eventEmitter.emit(HomeKitDevice.GET + '->' + deviceUUID, values);
2935
+ return values;
2895
2936
  }
2896
2937
 
2897
2938
  async #getWeatherData(connectionUUID, deviceUUID, latitude, longitude) {
@@ -2934,7 +2975,7 @@ export default class NestAccfactory {
2934
2975
  : '';
2935
2976
  })
2936
2977
  .catch((error) => {
2937
- if (error?.name !== 'TimeoutError' && this?.log?.debug) {
2978
+ if (error?.cause !== undefined && JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false && this?.log?.debug) {
2938
2979
  this.log.debug('REST API failed to retrieve weather details for uuid "%s". Error was "%s"', deviceUUID, error?.code);
2939
2980
  }
2940
2981
  });
@@ -2945,7 +2986,7 @@ export default class NestAccfactory {
2945
2986
 
2946
2987
  async #protobufCommand(connectionUUID, service, command, values) {
2947
2988
  if (
2948
- this.#connections?.[connectionUUID]?.protobufRoot === null ||
2989
+ this.#protobufRoot === null ||
2949
2990
  typeof service !== 'string' ||
2950
2991
  service === '' ||
2951
2992
  typeof command !== 'string' ||
@@ -2959,7 +3000,7 @@ export default class NestAccfactory {
2959
3000
  if (typeof object === 'object' && object !== null) {
2960
3001
  if ('type_url' in object && 'value' in object) {
2961
3002
  // We have a type_url and value object at this same level, we'll treat this a trait requiring encoding
2962
- let TraitMap = this.#connections[connectionUUID].protobufRoot.lookup(object.type_url.split('/')[1]);
3003
+ let TraitMap = this.#protobufRoot.lookup(object.type_url.split('/')[1]);
2963
3004
  if (TraitMap !== null) {
2964
3005
  object.value = TraitMap.encode(TraitMap.fromObject(object.value)).finish();
2965
3006
  }
@@ -2974,8 +3015,8 @@ export default class NestAccfactory {
2974
3015
  };
2975
3016
 
2976
3017
  // Attempt to retrieve both 'Request' and 'Reponse' traits for the associated service and command
2977
- let TraitMapRequest = this.#connections[connectionUUID].protobufRoot.lookup('nestlabs.gateway.v1.' + command + 'Request');
2978
- let TraitMapResponse = this.#connections[connectionUUID].protobufRoot.lookup('nestlabs.gateway.v1.' + command + 'Response');
3018
+ let TraitMapRequest = this.#protobufRoot.lookup('nestlabs.gateway.v1.' + command + 'Request');
3019
+ let TraitMapResponse = this.#protobufRoot.lookup('nestlabs.gateway.v1.' + command + 'Response');
2979
3020
  let commandResponse = undefined;
2980
3021
 
2981
3022
  if (TraitMapRequest !== null && TraitMapResponse !== null) {
@@ -3093,15 +3134,20 @@ function scaleValue(value, sourceRangeMin, sourceRangeMax, targetRangeMin, targe
3093
3134
  return ((value - sourceRangeMin) * (targetRangeMax - targetRangeMin)) / (sourceRangeMax - sourceRangeMin) + targetRangeMin;
3094
3135
  }
3095
3136
 
3096
- async function fetchWrapper(method, url, options, data) {
3137
+ async function fetchWrapper(method, url, options, data, response) {
3097
3138
  if ((method !== 'get' && method !== 'post') || typeof url !== 'string' || url === '' || typeof options !== 'object') {
3098
3139
  return;
3099
3140
  }
3100
3141
 
3101
- if (typeof options?.timeout === 'number' && options?.timeout > 0) {
3142
+ if (isNaN(options?.timeout) === false && Number(options?.timeout) > 0) {
3102
3143
  // If a timeout is specified in the options, setup here
3103
3144
  // eslint-disable-next-line no-undef
3104
- options.signal = AbortSignal.timeout(options.timeout);
3145
+ options.signal = AbortSignal.timeout(Number(options.timeout));
3146
+ }
3147
+
3148
+ if (options?.retry === undefined) {
3149
+ // If not retry option specifed , we'll do just once
3150
+ options.retry = 1;
3105
3151
  }
3106
3152
 
3107
3153
  options.method = method; // Set the HTTP method to use
@@ -3111,12 +3157,24 @@ async function fetchWrapper(method, url, options, data) {
3111
3157
  options.body = data;
3112
3158
  }
3113
3159
 
3114
- // eslint-disable-next-line no-undef
3115
- let response = await fetch(url, options);
3116
- if (response.ok === false) {
3117
- let error = new Error(response.statusText);
3118
- error.code = response.status;
3119
- throw error;
3160
+ if (options.retry > 0) {
3161
+ // eslint-disable-next-line no-undef
3162
+ response = await fetch(url, options);
3163
+ if (response.ok === false && options.retry > 1) {
3164
+ options.retry--; // One less retry to go
3165
+
3166
+ // Try again after short delay (500ms)
3167
+ // We pass back in this response also for when we reach zero retries and still not successful
3168
+ await new Promise((resolve) => setTimeout(resolve, 500));
3169
+ // eslint-disable-next-line no-undef
3170
+ response = await fetchWrapper(method, url, options, data, structuredClone(response));
3171
+ }
3172
+ if (response.ok === false && options.retry === 0) {
3173
+ let error = new Error(response.statusText);
3174
+ error.code = response.status;
3175
+ throw error;
3176
+ }
3120
3177
  }
3178
+
3121
3179
  return response;
3122
3180
  }