homebridge-nest-accfactory 0.0.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -4
- package/README.md +37 -19
- package/dist/HomeKitDevice.js +132 -109
- package/dist/camera.js +344 -262
- package/dist/doorbell.js +5 -3
- package/dist/floodlight.js +4 -4
- package/dist/nexustalk.js +84 -52
- package/dist/protect.js +2 -2
- package/dist/protobuf/googlehome/foyer.proto +216 -160
- package/dist/res/Nest_camera_connecting.h264 +0 -0
- package/dist/res/Nest_camera_off.h264 +0 -0
- package/dist/res/Nest_camera_offline.h264 +0 -0
- package/dist/res/Nest_camera_transfer.h264 +0 -0
- package/dist/streamer.js +100 -71
- package/dist/system.js +1321 -1263
- package/dist/thermostat.js +73 -27
- package/dist/webrtc.js +582 -0
- package/package.json +31 -29
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
|
|
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: '
|
|
58
|
+
PROTOBUF: 'Protobuf', // From the Protobuf API
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
static
|
|
62
|
-
static
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
162
|
+
if (fs.existsSync(this.config.options.ffmpeg.binary) === false) {
|
|
153
163
|
if (this?.log?.warn) {
|
|
154
|
-
this.log.warn('
|
|
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.
|
|
169
|
+
this.config.options.ffmpeg.binary = undefined;
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
if (fs.existsSync(
|
|
163
|
-
let ffmpegProcess = child_process.spawnSync(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
|
223
|
-
|
|
224
|
-
this.#eventEmitter.removeAllListeners(
|
|
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.#
|
|
244
|
-
|
|
245
|
-
this.#
|
|
246
|
-
|
|
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].
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 (
|
|
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 (
|
|
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 (
|
|
623
|
-
|
|
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(
|
|
738
|
+
await this.#processPostSubscribe();
|
|
663
739
|
})
|
|
664
740
|
.catch((error) => {
|
|
665
|
-
if (error?.
|
|
666
|
-
this.log.debug('REST API had an error performing 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
|
|
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.#
|
|
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.#
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
846
|
-
this.#rawData[
|
|
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[
|
|
853
|
-
|
|
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
|
-
|
|
856
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
877
|
-
deviceChanges = []; // No more device changes now
|
|
878
|
-
}
|
|
959
|
+
await this.#processPostSubscribe();
|
|
879
960
|
}
|
|
880
961
|
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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(
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
905
|
-
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
949
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1038
|
-
|
|
1105
|
+
error?.cause !== undefined &&
|
|
1106
|
+
JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
|
|
1107
|
+
this?.log?.debug
|
|
1039
1108
|
) {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
|
1263
|
-
const
|
|
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.
|
|
1268
|
-
data.
|
|
1269
|
-
|
|
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.
|
|
1276
|
-
|
|
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
|
|
1292
|
-
|
|
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
|
-
|
|
1297
|
-
this.config
|
|
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.
|
|
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.
|
|
1340
|
-
RESTTypeData.
|
|
1341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1420
|
-
|
|
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.
|
|
1482
|
-
value.value.fan_control_settings.timerSpeed.includes('FAN_SPEED_SETTING_STAGE') === true
|
|
1483
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
1566
|
-
RESTTypeData.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1686
|
-
value.value.fan_timer_speed.includes('stage') === true
|
|
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
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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.
|
|
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.
|
|
1751
|
-
tempDevice.humiditySensor = this.config?.devices?.[tempDevice.
|
|
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.
|
|
1754
|
-
? this.config.devices[tempDevice.
|
|
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.
|
|
1758
|
-
? this.config.devices[tempDevice.
|
|
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.
|
|
1762
|
-
? this.config.devices[tempDevice.
|
|
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.
|
|
1766
|
-
? this.config.devices[tempDevice.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1888
|
-
devices[tempDevice.
|
|
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
|
|
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.
|
|
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.
|
|
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 (
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
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.
|
|
2016
|
-
RESTTypeData.
|
|
2017
|
-
RESTTypeData.
|
|
2018
|
-
|
|
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.
|
|
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.
|
|
2053
|
-
devices[tempDevice.
|
|
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
|
|
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 (
|
|
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
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
RESTTypeData.
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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
|
|
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:
|
|
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 (
|
|
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.
|
|
2241
|
-
RESTTypeData.
|
|
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 =
|
|
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.
|
|
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.
|
|
2284
|
-
tempDevice.hksv = this.config.options.hksv === true || this.config?.devices?.[tempDevice.
|
|
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
|
-
|
|
2287
|
-
? this.config.devices[tempDevice.
|
|
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
|
-
|
|
2291
|
-
? this.config.devices[tempDevice.
|
|
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
|
-
|
|
2295
|
-
? this.config.devices[tempDevice.
|
|
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.
|
|
2298
|
-
tempDevice.localAccess = this.config?.devices?.[tempDevice.
|
|
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
|
-
|
|
2302
|
-
|
|
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 =
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
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) &&
|
|
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 (
|
|
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
|
|
2397
|
-
|
|
2398
|
-
tempDevice
|
|
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
|
-
|
|
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.
|
|
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.
|
|
2422
|
-
devices[tempDevice.
|
|
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(
|
|
2404
|
+
async #set(uuid, values) {
|
|
2430
2405
|
if (
|
|
2431
|
-
typeof
|
|
2432
|
-
|
|
2406
|
+
typeof uuid !== 'string' ||
|
|
2407
|
+
uuid === '' ||
|
|
2433
2408
|
typeof values !== 'object' ||
|
|
2434
|
-
|
|
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
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
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:
|
|
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)
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
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 === '
|
|
2487
|
-
|
|
2488
|
-
|
|
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
|
-
//
|
|
2491
|
-
protobufElement.
|
|
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 === '
|
|
2495
|
-
|
|
2496
|
-
|
|
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
|
-
//
|
|
2499
|
-
protobufElement.
|
|
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 === '
|
|
2503
|
-
|
|
2504
|
-
protobufElement.
|
|
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 === '
|
|
2508
|
-
|
|
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
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
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
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
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
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
}
|
|
2590
|
+
if (key === 'irled_enabled' && typeof value === 'string') {
|
|
2591
|
+
// 'auto_on'
|
|
2592
|
+
// 'always_off'
|
|
2593
|
+
}
|
|
2592
2594
|
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
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
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
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
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
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
|
-
|
|
2655
|
-
|
|
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
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
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
|
-
|
|
2670
|
-
|
|
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
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
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(
|
|
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"',
|
|
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?.[
|
|
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)
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
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
|
-
|
|
2706
|
-
|
|
2706
|
+
// Transform key to correct set camera properties key
|
|
2707
|
+
key = SETPROPERTIES[key] !== undefined ? SETPROPERTIES[key] : key;
|
|
2707
2708
|
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
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
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
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?.[
|
|
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)
|
|
2743
|
-
|
|
2750
|
+
Object.entries(values)
|
|
2751
|
+
.filter(([key]) => key !== 'uuid')
|
|
2752
|
+
.map(async ([key, value]) => {
|
|
2753
|
+
let subscribeJSONData = { objects: [] };
|
|
2744
2754
|
|
|
2745
|
-
|
|
2746
|
-
|
|
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
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
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
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
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
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
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
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
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(
|
|
2816
|
+
async #get(uuid, values) {
|
|
2793
2817
|
if (
|
|
2794
|
-
typeof
|
|
2795
|
-
|
|
2818
|
+
typeof uuid !== 'string' ||
|
|
2819
|
+
uuid === '' ||
|
|
2796
2820
|
typeof values !== 'object' ||
|
|
2797
|
-
|
|
2821
|
+
values?.uuid === undefined ||
|
|
2822
|
+
typeof this.#rawData?.[values?.uuid] !== 'object' ||
|
|
2823
|
+
typeof this.#connections?.[this.#rawData?.[values?.uuid]?.connection] !== 'object'
|
|
2798
2824
|
) {
|
|
2799
|
-
|
|
2825
|
+
return;
|
|
2800
2826
|
}
|
|
2801
2827
|
|
|
2802
|
-
|
|
2803
|
-
|
|
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
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
key
|
|
2846
|
-
|
|
2847
|
-
|
|
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
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
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
|
-
//
|
|
2871
|
-
await fetchWrapper(
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
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
|
-
|
|
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 (
|
|
2885
|
-
|
|
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
|
-
|
|
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?.
|
|
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.#
|
|
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.#
|
|
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.#
|
|
2978
|
-
let TraitMapResponse = this.#
|
|
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 (
|
|
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
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
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
|
}
|