homebridge-nest-accfactory 0.0.4-a → 0.0.5
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 +8 -1
- package/README.md +7 -7
- package/dist/HomeKitDevice.js +21 -10
- package/dist/HomeKitHistory.js +2 -24
- package/dist/camera.js +224 -236
- package/dist/doorbell.js +4 -4
- package/dist/floodlight.js +97 -0
- package/dist/index.js +7 -7
- package/dist/nexustalk.js +219 -418
- package/dist/protect.js +8 -9
- package/dist/protobuf/google/trait/product/camera.proto +1 -0
- package/dist/protobuf/googlehome/foyer.proto +11 -3
- package/dist/protobuf/nest/nexustalk.proto +181 -0
- package/dist/protobuf/nestlabs/eventingapi/v1.proto +6 -2
- package/dist/protobuf/nestlabs/gateway/v1.proto +29 -23
- package/dist/protobuf/nestlabs/gateway/v2.proto +16 -8
- package/dist/protobuf/root.proto +2 -27
- package/dist/protobuf/weave/trait/actuator.proto +13 -0
- package/dist/streamer.js +33 -30
- package/dist/system.js +1105 -1095
- package/dist/thermostat.js +5 -6
- package/package.json +7 -6
- package/dist/protobuf/nest/messages.proto +0 -8
- package/dist/webrtc.js +0 -55
package/dist/system.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// Nest System communications
|
|
2
2
|
// Part of homebridge-nest-accfactory
|
|
3
3
|
//
|
|
4
|
-
// Code version
|
|
4
|
+
// Code version 13/9/2024
|
|
5
5
|
// Mark Hulskamp
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
8
|
// Define external module requirements
|
|
9
|
-
import axios from 'axios';
|
|
10
9
|
import protobuf from 'protobufjs';
|
|
11
10
|
|
|
12
11
|
// Define nodejs module requirements
|
|
@@ -24,6 +23,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
24
23
|
import HomeKitDevice from './HomeKitDevice.js';
|
|
25
24
|
import NestCamera from './camera.js';
|
|
26
25
|
import NestDoorbell from './doorbell.js';
|
|
26
|
+
import NestFloodlight from './floodlight.js';
|
|
27
27
|
import NestProtect from './protect.js';
|
|
28
28
|
import NestTemperatureSensor from './tempsensor.js';
|
|
29
29
|
import NestWeather from './weather.js';
|
|
@@ -47,6 +47,7 @@ export default class NestAccfactory {
|
|
|
47
47
|
SMOKESENSOR: 'protect',
|
|
48
48
|
CAMERA: 'camera',
|
|
49
49
|
DOORBELL: 'doorbell',
|
|
50
|
+
FLOODLIGHT: 'floodlight',
|
|
50
51
|
WEATHER: 'weather',
|
|
51
52
|
LOCK: 'lock', // yet to implement
|
|
52
53
|
ALARM: 'alarm', // yet to implement
|
|
@@ -59,13 +60,11 @@ export default class NestAccfactory {
|
|
|
59
60
|
|
|
60
61
|
static GoogleConnection = 'google'; // Google account connection
|
|
61
62
|
static NestConnection = 'nest'; // Nest account connection
|
|
62
|
-
static SDMConnection = 'sdm'; // NOT coded, but here for future reference
|
|
63
|
-
static HomeFoyerConnection = 'foyer'; // Google Home foyer connection
|
|
64
63
|
|
|
65
64
|
cachedAccessories = []; // Track restored cached accessories
|
|
66
65
|
|
|
67
66
|
// Internal data only for this class
|
|
68
|
-
#connections = {}; //
|
|
67
|
+
#connections = {}; // Object of confirmed connections
|
|
69
68
|
#rawData = {}; // Cached copy of data from both Rest and Protobuf APIs
|
|
70
69
|
#eventEmitter = new EventEmitter(); // Used for object messaging from this platform
|
|
71
70
|
|
|
@@ -75,18 +74,48 @@ export default class NestAccfactory {
|
|
|
75
74
|
this.api = api;
|
|
76
75
|
|
|
77
76
|
// Perform validation on the configuration passed into us and set defaults if not present
|
|
78
|
-
if (typeof this.config?.nest !== 'object') {
|
|
79
|
-
this.config.nest = {};
|
|
80
|
-
}
|
|
81
|
-
this.config.nest.access_token = typeof this.config.nest?.access_token === 'string' ? this.config.nest.access_token : '';
|
|
82
|
-
this.config.nest.fieldTest = typeof this.config.nest?.fieldTest === 'boolean' ? this.config.nest.fieldTest : false;
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
// Build our accounts connection object. Allows us to have multiple diffent accont connections under the one accessory
|
|
79
|
+
Object.keys(this.config).forEach((key) => {
|
|
80
|
+
if (this.config[key]?.access_token !== undefined && this.config[key].access_token !== '') {
|
|
81
|
+
// Nest account connection, assign a random UUID for each connection
|
|
82
|
+
this.#connections[crypto.randomUUID()] = {
|
|
83
|
+
type: NestAccfactory.NestConnection,
|
|
84
|
+
authorised: false,
|
|
85
|
+
access_token: this.config[key].access_token,
|
|
86
|
+
fieldTest: this.config[key]?.fieldTest === true,
|
|
87
|
+
referer: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
88
|
+
restAPIHost: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
89
|
+
cameraAPIHost: this.config[key]?.fieldTest === true ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
|
|
90
|
+
protobufAPIHost: this.config[key]?.fieldTest === true ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
this.config[key]?.issuetoken !== undefined &&
|
|
95
|
+
this.config[key].issuetoken !== '' &&
|
|
96
|
+
this.config[key]?.cookie !== undefined &&
|
|
97
|
+
this.config[key].cookie !== ''
|
|
98
|
+
) {
|
|
99
|
+
// Google account connection, assign a random UUID for each connection
|
|
100
|
+
this.#connections[crypto.randomUUID()] = {
|
|
101
|
+
type: NestAccfactory.GoogleConnection,
|
|
102
|
+
authorised: false,
|
|
103
|
+
issuetoken: this.config[key].issuetoken,
|
|
104
|
+
cookie: this.config[key].cookie,
|
|
105
|
+
fieldTest: typeof this.config[key]?.fieldTest === 'boolean' ? this.config[key].fieldTest : false,
|
|
106
|
+
referer: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
107
|
+
restAPIHost: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
108
|
+
cameraAPIHost: this.config[key]?.fieldTest === true ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
|
|
109
|
+
protobufAPIHost: this.config[key]?.fieldTest === true ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// If we don't have either a Nest access_token and/or a Google issuetoken/cookie, return back.
|
|
115
|
+
if (Object.keys(this.#connections).length === 0) {
|
|
116
|
+
this?.log?.error && this.log.error('No connections have been specified in the JSON configuration. Please review');
|
|
117
|
+
return;
|
|
86
118
|
}
|
|
87
|
-
this.config.google.issuetoken = typeof this.config.google?.issuetoken === 'string' ? this.config.google.issuetoken : '';
|
|
88
|
-
this.config.google.cookie = typeof this.config.google?.cookie === 'string' ? this.config.google.cookie : '';
|
|
89
|
-
this.config.google.fieldTest = typeof this.config.google?.fieldTest === 'boolean' ? this.config.google.fieldTest : false;
|
|
90
119
|
|
|
91
120
|
if (typeof this.config?.options !== 'object') {
|
|
92
121
|
this.config.options = {};
|
|
@@ -183,12 +212,6 @@ export default class NestAccfactory {
|
|
|
183
212
|
}
|
|
184
213
|
}
|
|
185
214
|
|
|
186
|
-
// If we don't have either a Nest access_token and/or a Google issuetoken/cookie, return back.
|
|
187
|
-
if (this.config.nest.access_token === '' && (this.config.google.issuetoken === '' || this.config.google.cookie === '')) {
|
|
188
|
-
this?.log?.error && this.log.error('JSON plugin configuration is invalid. Please review');
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
215
|
if (this.api instanceof EventEmitter === true) {
|
|
193
216
|
this.api.on('didFinishLaunching', async () => {
|
|
194
217
|
// We got notified that Homebridge has finished loading, so we are ready to process
|
|
@@ -212,249 +235,188 @@ export default class NestAccfactory {
|
|
|
212
235
|
}
|
|
213
236
|
|
|
214
237
|
async discoverDevices() {
|
|
215
|
-
await this.#connect();
|
|
216
|
-
if (this.#connections?.nest !== undefined) {
|
|
217
|
-
// We have a 'Nest' connected account, so process accordingly
|
|
218
|
-
this.#subscribeREST(NestAccfactory.NestConnection, false);
|
|
219
|
-
this.#subscribeProtobuf(NestAccfactory.NestConnection);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (this.#connections?.google !== undefined) {
|
|
223
|
-
// We have a 'Google' connected account, so process accordingly
|
|
224
|
-
this.#subscribeREST(NestAccfactory.GoogleConnection, false);
|
|
225
|
-
this.#subscribeProtobuf(NestAccfactory.GoogleConnection);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
238
|
// Setup event listeners for set/get calls from devices
|
|
229
239
|
this.#eventEmitter.addListener(HomeKitDevice.SET, (deviceUUID, values) => this.#set(deviceUUID, values));
|
|
230
240
|
this.#eventEmitter.addListener(HomeKitDevice.GET, (deviceUUID, values) => this.#get(deviceUUID, values));
|
|
241
|
+
|
|
242
|
+
Object.keys(this.#connections).forEach((uuid) => {
|
|
243
|
+
this.#connect(uuid).then(() => {
|
|
244
|
+
if (this.#connections[uuid].authorised === true) {
|
|
245
|
+
this.#subscribeREST(uuid, true);
|
|
246
|
+
this.#subscribeProtobuf(uuid);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
231
250
|
}
|
|
232
251
|
|
|
233
|
-
async #connect() {
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
async #connect(connectionUUID) {
|
|
253
|
+
if (typeof this.#connections?.[connectionUUID] === 'object') {
|
|
254
|
+
this.#connections[connectionUUID].authorised === false; // Mark connection as no-longer authorised
|
|
255
|
+
if (this.#connections[connectionUUID].type === NestAccfactory.GoogleConnection) {
|
|
256
|
+
// Google cookie method as refresh token method no longer supported by Google since October 2022
|
|
257
|
+
// Instructions from homebridge_nest or homebridge_nest_cam to obtain this
|
|
258
|
+
this?.log?.info &&
|
|
259
|
+
this.log.info(
|
|
260
|
+
'Performing Google account authorisation ' +
|
|
261
|
+
(this.#connections[connectionUUID].fieldTest === true ? 'using field test endpoints' : ''),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await fetchWrapper('get', this.#connections[connectionUUID].issuetoken, {
|
|
265
|
+
headers: {
|
|
266
|
+
referer: 'https://accounts.google.com/o/oauth2/iframe',
|
|
267
|
+
'User-Agent': USERAGENT,
|
|
268
|
+
cookie: this.#connections[connectionUUID].cookie,
|
|
269
|
+
'Sec-Fetch-Mode': 'cors',
|
|
270
|
+
'X-Requested-With': 'XmlHttpRequest',
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
.then((response) => response.json())
|
|
274
|
+
.then(async (data) => {
|
|
275
|
+
let googleOAuth2Token = data.access_token;
|
|
276
|
+
|
|
277
|
+
await fetchWrapper(
|
|
278
|
+
'post',
|
|
279
|
+
'https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt',
|
|
280
|
+
{
|
|
281
|
+
headers: {
|
|
282
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
283
|
+
'User-Agent': USERAGENT,
|
|
284
|
+
Authorization: data.token_type + ' ' + data.access_token,
|
|
285
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
'embed_google_oauth_access_token=true&expire_after=3600s&google_oauth_access_token=' +
|
|
289
|
+
data.access_token +
|
|
290
|
+
'&policy_id=authproxy-oauth-policy',
|
|
291
|
+
)
|
|
292
|
+
.then((response) => response.json())
|
|
293
|
+
.then(async (data) => {
|
|
294
|
+
let googleToken = data.jwt;
|
|
295
|
+
let tokenExpire = Math.floor(new Date(data.claims.expirationTime).valueOf() / 1000); // Token expiry, should be 1hr
|
|
296
|
+
|
|
297
|
+
await fetchWrapper('get', 'https://' + this.#connections[connectionUUID].restAPIHost + '/session', {
|
|
298
|
+
headers: {
|
|
299
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
300
|
+
'User-Agent': USERAGENT,
|
|
301
|
+
Authorization: 'Basic ' + googleToken,
|
|
302
|
+
},
|
|
303
|
+
})
|
|
304
|
+
.then((response) => response.json())
|
|
305
|
+
.then((data) => {
|
|
306
|
+
// Store successful connection details
|
|
307
|
+
this.#connections[connectionUUID].authorised = true;
|
|
308
|
+
this.#connections[connectionUUID].userID = data.userid;
|
|
309
|
+
this.#connections[connectionUUID].transport_url = data.urls.transport_url;
|
|
310
|
+
this.#connections[connectionUUID].weather_url = data.urls.weather_url;
|
|
311
|
+
this.#connections[connectionUUID].protobufRoot = null;
|
|
312
|
+
this.#connections[connectionUUID].token = googleToken;
|
|
313
|
+
this.#connections[connectionUUID].cameraAPI = {
|
|
314
|
+
key: 'Authorization',
|
|
315
|
+
value: 'Basic ',
|
|
316
|
+
token: googleToken,
|
|
317
|
+
oauth2: googleOAuth2Token,
|
|
318
|
+
};
|
|
256
319
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
320
|
+
// Set timeout for token expiry refresh
|
|
321
|
+
this.#connections[connectionUUID].timer = clearInterval(this.#connections[connectionUUID].timer);
|
|
322
|
+
this.#connections[connectionUUID].timer = setTimeout(
|
|
323
|
+
() => {
|
|
324
|
+
this?.log?.info && this.log.info('Performing periodic token refresh for Google account');
|
|
325
|
+
this.#connect(connectionUUID);
|
|
326
|
+
},
|
|
327
|
+
(tokenExpire - Math.floor(Date.now() / 1000) - 60) * 1000,
|
|
328
|
+
); // Refresh just before token expiry
|
|
260
329
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
};
|
|
272
|
-
await axios(request)
|
|
273
|
-
.then(async (response) => {
|
|
274
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
275
|
-
throw new Error('Google API Authorisation failed with error');
|
|
276
|
-
}
|
|
277
|
-
this.special = response.data.access_token;
|
|
330
|
+
this?.log?.success && this.log.success('Successfully authorised using Google account');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
})
|
|
334
|
+
// eslint-disable-next-line no-unused-vars
|
|
335
|
+
.catch((error) => {
|
|
336
|
+
// The token we used to obtained a Nest session failed, so overall authorisation failed
|
|
337
|
+
this?.log?.error && this.log.error('Authorisation failed using Google account');
|
|
338
|
+
});
|
|
339
|
+
}
|
|
278
340
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
341
|
+
if (this.#connections[connectionUUID].type === NestAccfactory.NestConnection) {
|
|
342
|
+
// Nest access token method. Get WEBSITE2 cookie for use with camera API calls if needed later
|
|
343
|
+
this?.log?.info &&
|
|
344
|
+
this.log.info(
|
|
345
|
+
'Performing Nest account authorisation ' +
|
|
346
|
+
(this.#connections[connectionUUID].fieldTest === true ? 'using field test endpoints' : ''),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
await fetchWrapper(
|
|
350
|
+
'post',
|
|
351
|
+
'https://webapi.' + this.#connections[connectionUUID].cameraAPIHost + '/api/v1/login.login_nest',
|
|
352
|
+
{
|
|
353
|
+
withCredentials: true,
|
|
282
354
|
headers: {
|
|
283
|
-
referer: 'https://' + referer,
|
|
355
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
284
356
|
'User-Agent': USERAGENT,
|
|
285
|
-
|
|
357
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
286
358
|
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
295
|
-
throw new Error('Google Camera API Token get failed with error');
|
|
359
|
+
},
|
|
360
|
+
Buffer.from('access_token=' + this.#connections[connectionUUID].access_token, 'utf8'),
|
|
361
|
+
)
|
|
362
|
+
.then((response) => response.json())
|
|
363
|
+
.then(async (data) => {
|
|
364
|
+
if (data?.items?.[0]?.session_token === undefined) {
|
|
365
|
+
throw new Error('No Nest session token was obtained');
|
|
296
366
|
}
|
|
297
367
|
|
|
298
|
-
let
|
|
299
|
-
let tokenExpire = Math.floor(new Date(response.data.claims.expirationTime).valueOf() / 1000); // Token expiry, should be 1hr
|
|
368
|
+
let nestToken = data.items[0].session_token;
|
|
300
369
|
|
|
301
|
-
|
|
302
|
-
method: 'get',
|
|
303
|
-
url: 'https://' + restAPIHost + '/session',
|
|
370
|
+
await fetchWrapper('get', 'https://' + this.#connections[connectionUUID].restAPIHost + '/session', {
|
|
304
371
|
headers: {
|
|
305
|
-
referer: 'https://' + referer,
|
|
372
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
306
373
|
'User-Agent': USERAGENT,
|
|
307
|
-
Authorization: 'Basic ' +
|
|
374
|
+
Authorization: 'Basic ' + this.#connections[connectionUUID].access_token,
|
|
308
375
|
},
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
376
|
+
})
|
|
377
|
+
.then((response) => response.json())
|
|
378
|
+
.then((data) => {
|
|
379
|
+
// Store successful connection details
|
|
380
|
+
this.#connections[connectionUUID].authorised = true;
|
|
381
|
+
this.#connections[connectionUUID].userID = data.userid;
|
|
382
|
+
this.#connections[connectionUUID].transport_url = data.urls.transport_url;
|
|
383
|
+
this.#connections[connectionUUID].weather_url = data.urls.weather_url;
|
|
384
|
+
this.#connections[connectionUUID].protobufRoot = null;
|
|
385
|
+
this.#connections[connectionUUID].token = this.#connections[connectionUUID].access_token;
|
|
386
|
+
this.#connections[connectionUUID].cameraAPI = {
|
|
387
|
+
key: 'cookie',
|
|
388
|
+
value: this.#connections[connectionUUID].fieldTest === true ? 'website_ft=' : 'website_2=',
|
|
389
|
+
token: nestToken,
|
|
390
|
+
};
|
|
315
391
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
value: 'Basic ',
|
|
334
|
-
token: googleToken,
|
|
335
|
-
},
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
// Set timeout for token expiry refresh
|
|
339
|
-
clearInterval(this.#connections['google'].timer);
|
|
340
|
-
this.#connections['google'].timer = setTimeout(
|
|
341
|
-
() => {
|
|
342
|
-
this?.log?.info && this.log.info('Performing periodic token refresh for Google account');
|
|
343
|
-
this.#connect();
|
|
344
|
-
},
|
|
345
|
-
(tokenExpire - Math.floor(Date.now() / 1000) - 60) * 1000,
|
|
346
|
-
); // Refresh just before token expiry
|
|
347
|
-
});
|
|
392
|
+
// Set timeout for token expiry refresh
|
|
393
|
+
this.#connections[connectionUUID].timer = clearInterval(this.#connections[connectionUUID].timer);
|
|
394
|
+
this.#connections[connectionUUID].timer = setTimeout(
|
|
395
|
+
() => {
|
|
396
|
+
this?.log?.info && this.log.info('Performing periodic token refresh for Nest account');
|
|
397
|
+
this.#connect(connectionUUID);
|
|
398
|
+
},
|
|
399
|
+
1000 * 3600 * 24,
|
|
400
|
+
); // Refresh token every 24hrs
|
|
401
|
+
|
|
402
|
+
this?.log?.success && this.log.success('Successfully authorised using Nest account');
|
|
403
|
+
});
|
|
404
|
+
})
|
|
405
|
+
// eslint-disable-next-line no-unused-vars
|
|
406
|
+
.catch((error) => {
|
|
407
|
+
// The token we used to obtained a Nest session failed, so overall authorisation failed
|
|
408
|
+
this?.log?.error && this.log.error('Authorisation failed using Nest account');
|
|
348
409
|
});
|
|
349
|
-
})
|
|
350
|
-
// eslint-disable-next-line no-unused-vars
|
|
351
|
-
.catch((error) => {
|
|
352
|
-
// The token we used to obtained a Nest session failed, so overall authorisation failed
|
|
353
|
-
this?.log?.error && this.log.error('Authorisation failed using Google account');
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (typeof this.config?.nest?.access_token === 'string' && this.config?.nest?.access_token !== '') {
|
|
358
|
-
let referer = 'home.nest.com'; // Which host is 'actually' doing the request
|
|
359
|
-
let restAPIHost = 'home.nest.com'; // Root URL for Nest system REST API
|
|
360
|
-
let cameraAPIHost = 'camera.home.nest.com'; // Root URL for Camera system API
|
|
361
|
-
let protobufAPIHost = 'grpc-web.production.nest.com'; // Root URL for Protobuf API
|
|
362
|
-
|
|
363
|
-
if (this.config?.nest.fieldTest === true) {
|
|
364
|
-
// FieldTest mode support enabled in configuration, so update default endpoints
|
|
365
|
-
// This is all 'untested'
|
|
366
|
-
this?.log?.info && this.log.info('Using FieldTest API endpoints for Nest account');
|
|
367
|
-
|
|
368
|
-
referer = 'home.ft.nest.com'; // Which host is 'actually' doing the request
|
|
369
|
-
restAPIHost = 'home.ft.nest.com'; // Root FT URL for Nest system REST API
|
|
370
|
-
cameraAPIHost = 'camera.home.ft.nest.com'; // Root FT URL for Camera system API
|
|
371
|
-
protobufAPIHost = 'grpc-web.ft.nest.com'; // Root FT URL for Protobuf API
|
|
372
410
|
}
|
|
373
|
-
|
|
374
|
-
// Nest access token method. Get WEBSITE2 cookie for use with camera API calls if needed later
|
|
375
|
-
this?.log?.info && this.log.info('Performing Nest account authorisation');
|
|
376
|
-
|
|
377
|
-
let request = {
|
|
378
|
-
method: 'post',
|
|
379
|
-
url: 'https://webapi.' + cameraAPIHost + '/api/v1/login.login_nest',
|
|
380
|
-
withCredentials: true,
|
|
381
|
-
headers: {
|
|
382
|
-
referer: 'https://' + referer,
|
|
383
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
384
|
-
'User-Agent': USERAGENT,
|
|
385
|
-
},
|
|
386
|
-
data: Buffer.from('access_token=' + this.config.nest.access_token, 'utf8'),
|
|
387
|
-
};
|
|
388
|
-
await axios(request)
|
|
389
|
-
.then(async (response) => {
|
|
390
|
-
if (
|
|
391
|
-
typeof response.status !== 'number' ||
|
|
392
|
-
response.status !== 200 ||
|
|
393
|
-
typeof response.data.status !== 'number' ||
|
|
394
|
-
response.data.status !== 0
|
|
395
|
-
) {
|
|
396
|
-
throw new Error('Nest API Authorisation failed with error');
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
let nestToken = response.data.items[0].session_token;
|
|
400
|
-
|
|
401
|
-
let request = {
|
|
402
|
-
method: 'get',
|
|
403
|
-
url: 'https://' + restAPIHost + '/session',
|
|
404
|
-
headers: {
|
|
405
|
-
referer: 'https://' + referer,
|
|
406
|
-
'User-Agent': USERAGENT,
|
|
407
|
-
Authorization: 'Basic ' + this.config.nest.access_token,
|
|
408
|
-
},
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
await axios(request).then((response) => {
|
|
412
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
413
|
-
throw new Error('Nest Session API get failed with error');
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
this?.log?.success && this.log.success('Successfully authorised using Nest account');
|
|
417
|
-
|
|
418
|
-
// Store successful connection details
|
|
419
|
-
this.#connections['nest'] = {
|
|
420
|
-
type: 'nest',
|
|
421
|
-
referer: referer,
|
|
422
|
-
restAPIHost: restAPIHost,
|
|
423
|
-
cameraAPIHost: cameraAPIHost,
|
|
424
|
-
protobufAPIHost: protobufAPIHost,
|
|
425
|
-
userID: response.data.userid,
|
|
426
|
-
transport_url: response.data.urls.transport_url,
|
|
427
|
-
weather_url: response.data.urls.weather_url,
|
|
428
|
-
timer: null,
|
|
429
|
-
protobufRoot: null,
|
|
430
|
-
token: this.config.nest.access_token,
|
|
431
|
-
cameraAPI: {
|
|
432
|
-
key: 'cookie',
|
|
433
|
-
value: this.config.fieldTest === true ? 'website_ft=' : 'website_2=',
|
|
434
|
-
token: nestToken,
|
|
435
|
-
},
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
// Set timeout for token expiry refresh
|
|
439
|
-
clearInterval(this.#connections['nest'].timer);
|
|
440
|
-
this.#connections['nest'].timer = setTimeout(
|
|
441
|
-
() => {
|
|
442
|
-
this?.log?.info && this.log.info('Performing periodic token refresh for Nest account');
|
|
443
|
-
this.#connect();
|
|
444
|
-
},
|
|
445
|
-
1000 * 3600 * 24,
|
|
446
|
-
); // Refresh token every 24hrs
|
|
447
|
-
});
|
|
448
|
-
})
|
|
449
|
-
// eslint-disable-next-line no-unused-vars
|
|
450
|
-
.catch((error) => {
|
|
451
|
-
// The token we used to obtained a Nest session failed, so overall authorisation failed
|
|
452
|
-
this?.log?.error && this.log.error('Authorisation failed using Nest account');
|
|
453
|
-
});
|
|
454
411
|
}
|
|
455
412
|
}
|
|
456
413
|
|
|
457
|
-
async #subscribeREST(
|
|
414
|
+
async #subscribeREST(connectionUUID, fullRefresh) {
|
|
415
|
+
if (typeof this.#connections?.[connectionUUID] !== 'object' || this.#connections?.[connectionUUID]?.authorised !== true) {
|
|
416
|
+
// Not a valid connection object and/or we're not authorised
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
458
420
|
const REQUIREDBUCKETS = [
|
|
459
421
|
'buckets',
|
|
460
422
|
'structure',
|
|
@@ -479,64 +441,56 @@ export default class NestAccfactory {
|
|
|
479
441
|
quartz: ['where_id', 'structure_id', 'nexus_api_http_server_url'],
|
|
480
442
|
};
|
|
481
443
|
|
|
482
|
-
|
|
483
|
-
let
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
this.#rawData[object_key]?.source === NestAccfactory.DataSource.REST &&
|
|
502
|
-
this.#rawData[object_key]?.connection === connectionType &&
|
|
503
|
-
typeof this.#rawData[object_key]?.object_revision === 'number' &&
|
|
504
|
-
typeof this.#rawData[object_key]?.object_timestamp === 'number'
|
|
505
|
-
) {
|
|
506
|
-
restAPIJSONData.objects.push({
|
|
444
|
+
// By default, setup for a full data read from the REST API
|
|
445
|
+
let subscribeURL =
|
|
446
|
+
'https://' +
|
|
447
|
+
this.#connections[connectionUUID].restAPIHost +
|
|
448
|
+
'/api/0.1/user/' +
|
|
449
|
+
this.#connections[connectionUUID].userID +
|
|
450
|
+
'/app_launch';
|
|
451
|
+
let subscribeJSONData = { known_bucket_types: REQUIREDBUCKETS, known_bucket_versions: [] };
|
|
452
|
+
|
|
453
|
+
if (fullRefresh === false) {
|
|
454
|
+
// We have data stored from ths REST API, so setup read using known objects
|
|
455
|
+
subscribeURL = this.#connections[connectionUUID].transport_url + '/v6/subscribe';
|
|
456
|
+
subscribeJSONData = { objects: [] };
|
|
457
|
+
|
|
458
|
+
Object.entries(this.#rawData)
|
|
459
|
+
// eslint-disable-next-line no-unused-vars
|
|
460
|
+
.filter(([object_key, object]) => object.source === NestAccfactory.DataSource.REST && object.connection === connectionUUID)
|
|
461
|
+
.forEach(([object_key, object]) => {
|
|
462
|
+
subscribeJSONData.objects.push({
|
|
507
463
|
object_key: object_key,
|
|
508
|
-
object_revision:
|
|
509
|
-
object_timestamp:
|
|
464
|
+
object_revision: object.object_revision,
|
|
465
|
+
object_timestamp: object.object_timestamp,
|
|
510
466
|
});
|
|
511
|
-
}
|
|
512
|
-
});
|
|
467
|
+
});
|
|
513
468
|
}
|
|
514
469
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
470
|
+
fetchWrapper(
|
|
471
|
+
'post',
|
|
472
|
+
subscribeURL,
|
|
473
|
+
{
|
|
474
|
+
headers: {
|
|
475
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
476
|
+
'User-Agent': USERAGENT,
|
|
477
|
+
Authorization: 'Basic ' + this.#connections[connectionUUID].token,
|
|
478
|
+
},
|
|
479
|
+
keepalive: true,
|
|
480
|
+
timeout: 300 * 60,
|
|
522
481
|
},
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
.then(async (
|
|
527
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
528
|
-
throw new Error('REST API subscription failed with error');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
let data = {};
|
|
482
|
+
JSON.stringify(subscribeJSONData),
|
|
483
|
+
)
|
|
484
|
+
.then((response) => response.json())
|
|
485
|
+
.then(async (data) => {
|
|
532
486
|
let deviceChanges = []; // No REST API devices changes to start with
|
|
533
|
-
if (typeof
|
|
487
|
+
if (typeof data?.updated_buckets === 'object') {
|
|
534
488
|
// This response is full data read
|
|
535
|
-
data =
|
|
489
|
+
data = data.updated_buckets;
|
|
536
490
|
}
|
|
537
|
-
if (typeof
|
|
491
|
+
if (typeof data?.objects === 'object') {
|
|
538
492
|
// This response contains subscribed data updates
|
|
539
|
-
data =
|
|
493
|
+
data = data.objects;
|
|
540
494
|
}
|
|
541
495
|
|
|
542
496
|
// Process the data we received
|
|
@@ -555,7 +509,7 @@ export default class NestAccfactory {
|
|
|
555
509
|
value.value.weather = this.#rawData[value.object_key].value.weather;
|
|
556
510
|
}
|
|
557
511
|
value.value.weather = await this.#getWeatherData(
|
|
558
|
-
|
|
512
|
+
connectionUUID,
|
|
559
513
|
value.object_key,
|
|
560
514
|
value.value.latitude,
|
|
561
515
|
value.value.longitude,
|
|
@@ -580,34 +534,30 @@ export default class NestAccfactory {
|
|
|
580
534
|
? this.#rawData[value.object_key].value.properties
|
|
581
535
|
: [];
|
|
582
536
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
this.#connections[connectionType].cameraAPIHost +
|
|
537
|
+
await fetchWrapper(
|
|
538
|
+
'get',
|
|
539
|
+
'https://webapi.' +
|
|
540
|
+
this.#connections[connectionUUID].cameraAPIHost +
|
|
588
541
|
'/api/cameras.get_with_properties?uuid=' +
|
|
589
542
|
value.object_key.split('.')[1],
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
this.#connections[
|
|
543
|
+
{
|
|
544
|
+
headers: {
|
|
545
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
546
|
+
'User-Agent': USERAGENT,
|
|
547
|
+
[this.#connections[connectionUUID].cameraAPI.key]:
|
|
548
|
+
this.#connections[connectionUUID].cameraAPI.value + this.#connections[connectionUUID].cameraAPI.token,
|
|
549
|
+
},
|
|
550
|
+
timeout: NESTAPITIMEOUT,
|
|
595
551
|
},
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
.then((response) => {
|
|
601
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
602
|
-
throw new Error('REST API had error retrieving camera/doorbell details');
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
value.value.properties = response.data.items[0].properties;
|
|
552
|
+
)
|
|
553
|
+
.then((response) => response.json())
|
|
554
|
+
.then((data) => {
|
|
555
|
+
value.value.properties = data.items[0].properties;
|
|
606
556
|
})
|
|
607
557
|
.catch((error) => {
|
|
608
|
-
this?.log?.debug
|
|
609
|
-
this
|
|
610
|
-
|
|
558
|
+
if (error?.name !== 'TimeoutError' && this?.log?.debug) {
|
|
559
|
+
this.log.debug('REST API had error retrieving camera/doorbell details during subscribe. Error was "%s"', error?.code);
|
|
560
|
+
}
|
|
611
561
|
});
|
|
612
562
|
|
|
613
563
|
value.value.activity_zones =
|
|
@@ -615,26 +565,19 @@ export default class NestAccfactory {
|
|
|
615
565
|
? this.#rawData[value.object_key].value.activity_zones
|
|
616
566
|
: [];
|
|
617
567
|
|
|
618
|
-
|
|
619
|
-
method: 'get',
|
|
620
|
-
url: value.value.nexus_api_http_server_url + '/cuepoint_category/' + value.object_key.split('.')[1],
|
|
568
|
+
await fetchWrapper('get', value.value.nexus_api_http_server_url + '/cuepoint_category/' + value.object_key.split('.')[1], {
|
|
621
569
|
headers: {
|
|
622
|
-
referer: 'https://' + this.#connections[
|
|
570
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
623
571
|
'User-Agent': USERAGENT,
|
|
624
|
-
[this.#connections[
|
|
625
|
-
this.#connections[
|
|
572
|
+
[this.#connections[connectionUUID].cameraAPI.key]:
|
|
573
|
+
this.#connections[connectionUUID].cameraAPI.value + this.#connections[connectionUUID].cameraAPI.token,
|
|
626
574
|
},
|
|
627
|
-
responseType: 'json',
|
|
628
575
|
timeout: NESTAPITIMEOUT,
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
.then((
|
|
632
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
633
|
-
throw new Error('REST API had error retrieving camera/doorbell activity zones');
|
|
634
|
-
}
|
|
635
|
-
|
|
576
|
+
})
|
|
577
|
+
.then((response) => response.json())
|
|
578
|
+
.then((data) => {
|
|
636
579
|
let zones = [];
|
|
637
|
-
|
|
580
|
+
data.forEach((zone) => {
|
|
638
581
|
if (zone.type.toUpperCase() === 'ACTIVITY' || zone.type.toUpperCase() === 'REGION') {
|
|
639
582
|
zones.push({
|
|
640
583
|
id: zone.id === 0 ? 1 : zone.id,
|
|
@@ -648,9 +591,12 @@ export default class NestAccfactory {
|
|
|
648
591
|
value.value.activity_zones = zones;
|
|
649
592
|
})
|
|
650
593
|
.catch((error) => {
|
|
651
|
-
this?.log?.debug
|
|
652
|
-
this
|
|
653
|
-
|
|
594
|
+
if (error?.name !== 'TimeoutError' && this?.log?.debug) {
|
|
595
|
+
this.log.debug(
|
|
596
|
+
'REST API had error retrieving camera/doorbell activity zones during subscribe. Error was "%s"',
|
|
597
|
+
error?.code,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
654
600
|
});
|
|
655
601
|
}
|
|
656
602
|
|
|
@@ -687,7 +633,7 @@ export default class NestAccfactory {
|
|
|
687
633
|
this.#rawData[value.object_key] = {};
|
|
688
634
|
this.#rawData[value.object_key].object_revision = value.object_revision;
|
|
689
635
|
this.#rawData[value.object_key].object_timestamp = value.object_timestamp;
|
|
690
|
-
this.#rawData[value.object_key].connection =
|
|
636
|
+
this.#rawData[value.object_key].connection = connectionUUID;
|
|
691
637
|
this.#rawData[value.object_key].source = NestAccfactory.DataSource.REST;
|
|
692
638
|
this.#rawData[value.object_key].timers = {}; // No timers running for this object
|
|
693
639
|
this.#rawData[value.object_key].value = {};
|
|
@@ -716,16 +662,21 @@ export default class NestAccfactory {
|
|
|
716
662
|
await this.#processPostSubscribe(deviceChanges);
|
|
717
663
|
})
|
|
718
664
|
.catch((error) => {
|
|
719
|
-
if (error?.
|
|
720
|
-
this
|
|
665
|
+
if (error?.name !== 'TimeoutError' && this?.log?.debug) {
|
|
666
|
+
this.log.debug('REST API had an error performing subscription. Will restart subscription');
|
|
721
667
|
}
|
|
722
668
|
})
|
|
723
669
|
.finally(() => {
|
|
724
|
-
setTimeout(this.#subscribeREST.bind(this,
|
|
670
|
+
setTimeout(this.#subscribeREST.bind(this, connectionUUID, fullRefresh), 1000);
|
|
725
671
|
});
|
|
726
672
|
}
|
|
727
673
|
|
|
728
|
-
async #subscribeProtobuf(
|
|
674
|
+
async #subscribeProtobuf(connectionUUID) {
|
|
675
|
+
if (typeof this.#connections?.[connectionUUID] !== 'object' || this.#connections?.[connectionUUID]?.authorised !== true) {
|
|
676
|
+
// Not a valid connection object and/or we're not authorised
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
729
680
|
const calculate_message_size = (inputBuffer) => {
|
|
730
681
|
// First byte in the is a tag type??
|
|
731
682
|
// Following is a varint type
|
|
@@ -761,177 +712,182 @@ export default class NestAccfactory {
|
|
|
761
712
|
}
|
|
762
713
|
};
|
|
763
714
|
|
|
764
|
-
|
|
765
|
-
if (
|
|
715
|
+
// Attempt to load in protobuf files if not already done so for this connection
|
|
716
|
+
if (
|
|
717
|
+
this.#connections[connectionUUID].protobufRoot === null &&
|
|
718
|
+
fs.existsSync(path.resolve(__dirname + '/protobuf/root.proto')) === true
|
|
719
|
+
) {
|
|
766
720
|
protobuf.util.Long = null;
|
|
767
721
|
protobuf.configure();
|
|
768
|
-
this.#connections[
|
|
769
|
-
if (this.#connections[connectionType].protobufRoot !== null) {
|
|
770
|
-
// Loaded in the protobuf files, so now dynamically build the 'observe' post body data based on what we have loaded
|
|
771
|
-
let observeTraitsList = [];
|
|
772
|
-
let traitTypeObserveParam = this.#connections[connectionType].protobufRoot.lookup('nestlabs.gateway.v2.TraitTypeObserveParams');
|
|
773
|
-
let observeRequest = this.#connections[connectionType].protobufRoot.lookup('nestlabs.gateway.v2.ObserveRequest');
|
|
774
|
-
if (traitTypeObserveParam !== null && observeRequest !== null) {
|
|
775
|
-
traverseTypes(this.#connections[connectionType].protobufRoot, (type) => {
|
|
776
|
-
// We only want to have certain trait main 'families' in our observe reponse we are building
|
|
777
|
-
// This also depends on the account type we connected with. Nest accounts cannot observe camera/doorbell product traits
|
|
778
|
-
if (
|
|
779
|
-
(connectionType === NestAccfactory.NestConnection &&
|
|
780
|
-
type.fullName.startsWith('.nest.trait.product.camera') === false &&
|
|
781
|
-
type.fullName.startsWith('.nest.trait.product.doorbell') === false &&
|
|
782
|
-
(type.fullName.startsWith('.nest.trait') === true || type.fullName.startsWith('.weave.') === true)) ||
|
|
783
|
-
(connectionType === NestAccfactory.GoogleConnection &&
|
|
784
|
-
(type.fullName.startsWith('.nest.trait') === true ||
|
|
785
|
-
type.fullName.startsWith('.weave.') === true ||
|
|
786
|
-
type.fullName.startsWith('.google.trait.product.camera') === true))
|
|
787
|
-
) {
|
|
788
|
-
observeTraitsList.push(traitTypeObserveParam.create({ traitType: type.fullName.replace(/^\.*|\.*$/g, '') }));
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
observeTraits = observeRequest.encode(observeRequest.create({ stateTypes: [1, 2], traitTypeParams: observeTraitsList })).finish();
|
|
792
|
-
}
|
|
793
|
-
}
|
|
722
|
+
this.#connections[connectionUUID].protobufRoot = protobuf.loadSync(path.resolve(__dirname + '/protobuf/root.proto'));
|
|
794
723
|
}
|
|
795
724
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
try {
|
|
823
|
-
// Attempt to decode the protobuf message(s) we extracted from the stream and get a JSON object representation
|
|
824
|
-
decodedMessage = this.#connections[connectionType].protobufRoot
|
|
825
|
-
.lookup('nest.rpc.StreamBody')
|
|
826
|
-
.decode(buffer.subarray(0, messageSize))
|
|
827
|
-
.toJSON();
|
|
828
|
-
if (typeof decodedMessage?.message !== 'object') {
|
|
829
|
-
decodedMessage.message = [];
|
|
830
|
-
}
|
|
831
|
-
if (typeof decodedMessage?.message[0]?.get !== 'object') {
|
|
832
|
-
decodedMessage.message[0].get = [];
|
|
833
|
-
}
|
|
834
|
-
if (typeof decodedMessage?.message[0]?.resourceMetas !== 'object') {
|
|
835
|
-
decodedMessage.message[0].resourceMetas = [];
|
|
836
|
-
}
|
|
725
|
+
if (this.#connections[connectionUUID].protobufRoot !== null) {
|
|
726
|
+
// Loaded in the Protobuf files, so now dynamically build the 'observe' post body data based on what we have loaded
|
|
727
|
+
let observeTraitsList = [];
|
|
728
|
+
let observeBody = Buffer.alloc(0);
|
|
729
|
+
let traitTypeObserveParam = this.#connections[connectionUUID].protobufRoot.lookup('nestlabs.gateway.v2.TraitTypeObserveParams');
|
|
730
|
+
let observeRequest = this.#connections[connectionUUID].protobufRoot.lookup('nestlabs.gateway.v2.ObserveRequest');
|
|
731
|
+
if (traitTypeObserveParam !== null && observeRequest !== null) {
|
|
732
|
+
traverseTypes(this.#connections[connectionUUID].protobufRoot, (type) => {
|
|
733
|
+
// We only want to have certain trait'families' in our observe reponse we are building
|
|
734
|
+
// This also depends on the account type we connected with
|
|
735
|
+
// Nest accounts cannot observe camera/doorbell product traits
|
|
736
|
+
if (
|
|
737
|
+
(this.#connections[connectionUUID].type === NestAccfactory.NestConnection &&
|
|
738
|
+
type.fullName.startsWith('.nest.trait.product.camera') === false &&
|
|
739
|
+
type.fullName.startsWith('.nest.trait.product.doorbell') === false &&
|
|
740
|
+
(type.fullName.startsWith('.nest.trait') === true || type.fullName.startsWith('.weave.') === true)) ||
|
|
741
|
+
(this.#connections[connectionUUID].type === NestAccfactory.GoogleConnection &&
|
|
742
|
+
(type.fullName.startsWith('.nest.trait') === true ||
|
|
743
|
+
type.fullName.startsWith('.weave.') === true ||
|
|
744
|
+
type.fullName.startsWith('.google.trait.product.camera') === true))
|
|
745
|
+
) {
|
|
746
|
+
observeTraitsList.push(traitTypeObserveParam.create({ traitType: type.fullName.replace(/^\.*|\.*$/g, '') }));
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
observeBody = observeRequest.encode(observeRequest.create({ stateTypes: [1, 2], traitTypeParams: observeTraitsList })).finish();
|
|
750
|
+
}
|
|
837
751
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
752
|
+
fetchWrapper(
|
|
753
|
+
'post',
|
|
754
|
+
'https://' + this.#connections[connectionUUID].protobufAPIHost + '/nestlabs.gateway.v2.GatewayService/Observe',
|
|
755
|
+
{
|
|
756
|
+
headers: {
|
|
757
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
758
|
+
'User-Agent': USERAGENT,
|
|
759
|
+
Authorization: 'Basic ' + this.#connections[connectionUUID].token,
|
|
760
|
+
'Content-Type': 'application/x-protobuf',
|
|
761
|
+
'X-Accept-Content-Transfer-Encoding': 'binary',
|
|
762
|
+
'X-Accept-Response-Streaming': 'true',
|
|
763
|
+
},
|
|
764
|
+
keepalive: true,
|
|
765
|
+
timeout: 300 * 60,
|
|
766
|
+
},
|
|
767
|
+
observeBody,
|
|
768
|
+
)
|
|
769
|
+
.then((response) => response.body)
|
|
770
|
+
.then(async (data) => {
|
|
771
|
+
let deviceChanges = []; // No Protobuf API devices changes to start with
|
|
772
|
+
let buffer = Buffer.alloc(0);
|
|
773
|
+
for await (const chunk of data) {
|
|
774
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
775
|
+
let messageSize = calculate_message_size(buffer);
|
|
776
|
+
if (buffer.length >= messageSize) {
|
|
777
|
+
let decodedMessage = {};
|
|
778
|
+
try {
|
|
779
|
+
// Attempt to decode the Protobuf message(s) we extracted from the stream and get a JSON object representation
|
|
780
|
+
decodedMessage = this.#connections[connectionUUID].protobufRoot
|
|
781
|
+
.lookup('nestlabs.gateway.v2.ObserveResponse')
|
|
782
|
+
.decode(buffer.subarray(0, messageSize))
|
|
783
|
+
.toJSON();
|
|
784
|
+
|
|
785
|
+
// Tidy up our received messages. This ensures we only have one status for the trait in the data we process
|
|
786
|
+
// We'll favour a trait with accepted status over the same with confirmed status
|
|
787
|
+
if (decodedMessage?.observeResponse?.[0]?.traitStates !== undefined) {
|
|
788
|
+
let notAcceptedStatus = decodedMessage.observeResponse[0].traitStates.filter(
|
|
789
|
+
(trait) => trait.stateTypes.includes('ACCEPTED') === false,
|
|
790
|
+
);
|
|
791
|
+
let acceptedStatus = decodedMessage.observeResponse[0].traitStates.filter(
|
|
792
|
+
(trait) => trait.stateTypes.includes('ACCEPTED') === true,
|
|
793
|
+
);
|
|
794
|
+
let difference = acceptedStatus.map((trait) => trait.traitId.resourceId + '/' + trait.traitId.traitLabel);
|
|
795
|
+
decodedMessage.observeResponse[0].traitStates =
|
|
796
|
+
((notAcceptedStatus = notAcceptedStatus.filter(
|
|
797
|
+
(trait) => difference.includes(trait.traitId.resourceId + '/' + trait.traitId.traitLabel) === false,
|
|
798
|
+
)),
|
|
799
|
+
[...notAcceptedStatus, ...acceptedStatus]);
|
|
859
800
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
buffer = buffer.subarray(messageSize); // Remove the message from the beginning of the buffer
|
|
866
|
-
|
|
867
|
-
if (typeof decodedMessage?.message[0]?.get === 'object') {
|
|
868
|
-
await Promise.all(
|
|
869
|
-
decodedMessage.message[0].get.map(async (trait) => {
|
|
870
|
-
if (trait.traitId.traitLabel === 'configuration_done') {
|
|
801
|
+
// We'll use the resource status message to look for structure and/or device removals
|
|
802
|
+
// We could also check for structure and/or device additions here, but we'll want to be flagged
|
|
803
|
+
// that a device is 'ready' for use before we add in. This data is populated in the trait data
|
|
804
|
+
if (decodedMessage?.observeResponse?.[0]?.resourceMetas !== undefined) {
|
|
805
|
+
decodedMessage.observeResponse[0].resourceMetas.map(async (resource) => {
|
|
871
806
|
if (
|
|
872
|
-
|
|
873
|
-
|
|
807
|
+
resource.status === 'REMOVED' &&
|
|
808
|
+
(resource.resourceId.startsWith('STRUCTURE_') || resource.resourceId.startsWith('DEVICE_'))
|
|
874
809
|
) {
|
|
875
|
-
|
|
810
|
+
// We have the removal of a 'home' and/ device
|
|
811
|
+
deviceChanges.push({ object_key: resource.resourceId, change: 'removed' });
|
|
876
812
|
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
// eslint-disable-next-line no-unused-vars
|
|
816
|
+
} catch (error) {
|
|
817
|
+
// Empty
|
|
818
|
+
}
|
|
819
|
+
buffer = buffer.subarray(messageSize); // Remove the message from the beginning of the buffer
|
|
820
|
+
|
|
821
|
+
if (typeof decodedMessage?.observeResponse?.[0]?.traitStates === 'object') {
|
|
822
|
+
await Promise.all(
|
|
823
|
+
decodedMessage.observeResponse[0].traitStates.map(async (trait) => {
|
|
824
|
+
if (trait.traitId.traitLabel === 'configuration_done') {
|
|
825
|
+
if (
|
|
826
|
+
this.#rawData[trait.traitId.resourceId]?.value?.configuration_done?.deviceReady !== true &&
|
|
827
|
+
trait.patch.values?.deviceReady === true
|
|
828
|
+
) {
|
|
829
|
+
deviceChanges.push({ object_key: trait.traitId.resourceId, change: 'add' });
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (trait.traitId.traitLabel === 'camera_migration_status') {
|
|
833
|
+
// Handle case of camera/doorbell(s) which have been migrated from Nest to Google Home
|
|
834
|
+
if (
|
|
835
|
+
this.#rawData[trait.traitId.resourceId]?.value?.camera_migration_status?.state?.where !==
|
|
836
|
+
'MIGRATED_TO_GOOGLE_HOME' &&
|
|
837
|
+
trait.patch.values?.state?.where === 'MIGRATED_TO_GOOGLE_HOME' &&
|
|
838
|
+
this.#rawData[trait.traitId.resourceId]?.value?.camera_migration_status?.state?.progress !== 'PROGRESS_COMPLETE' &&
|
|
839
|
+
trait.patch.values?.state?.progress === 'PROGRESS_COMPLETE'
|
|
840
|
+
) {
|
|
841
|
+
deviceChanges.push({ object_key: trait.traitId.resourceId, change: 'add' });
|
|
842
|
+
}
|
|
887
843
|
}
|
|
888
|
-
}
|
|
889
844
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
845
|
+
if (typeof this.#rawData[trait.traitId.resourceId] === 'undefined') {
|
|
846
|
+
this.#rawData[trait.traitId.resourceId] = {};
|
|
847
|
+
this.#rawData[trait.traitId.resourceId].connection = connectionUUID;
|
|
848
|
+
this.#rawData[trait.traitId.resourceId].source = NestAccfactory.DataSource.PROTOBUF;
|
|
849
|
+
this.#rawData[trait.traitId.resourceId].timers = {}; // No timers running for this object
|
|
850
|
+
this.#rawData[trait.traitId.resourceId].value = {};
|
|
851
|
+
}
|
|
852
|
+
this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel] =
|
|
853
|
+
typeof trait.patch.values !== 'undefined' ? trait.patch.values : {};
|
|
899
854
|
|
|
900
|
-
|
|
901
|
-
|
|
855
|
+
// We don't need to store the trait type, so remove it
|
|
856
|
+
delete this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel]['@type'];
|
|
902
857
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
858
|
+
// If we have structure location details and associated geo-location details, get the weather data for the location
|
|
859
|
+
// We'll store this in the object key/value as per REST API
|
|
860
|
+
if (
|
|
861
|
+
trait.traitId.resourceId.startsWith('STRUCTURE_') === true &&
|
|
862
|
+
trait.traitId.traitLabel === 'structure_location' &&
|
|
863
|
+
typeof trait.patch.values?.geoCoordinate?.latitude === 'number' &&
|
|
864
|
+
typeof trait.patch.values?.geoCoordinate?.longitude === 'number'
|
|
865
|
+
) {
|
|
866
|
+
this.#rawData[trait.traitId.resourceId].value.weather = await this.#getWeatherData(
|
|
867
|
+
connectionUUID,
|
|
868
|
+
trait.traitId.resourceId,
|
|
869
|
+
trait.patch.values.geoCoordinate.latitude,
|
|
870
|
+
trait.patch.values.geoCoordinate.longitude,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
}),
|
|
874
|
+
);
|
|
920
875
|
|
|
921
|
-
|
|
922
|
-
|
|
876
|
+
await this.#processPostSubscribe(deviceChanges);
|
|
877
|
+
deviceChanges = []; // No more device changes now
|
|
878
|
+
}
|
|
923
879
|
}
|
|
924
880
|
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
881
|
+
})
|
|
882
|
+
.catch((error) => {
|
|
883
|
+
if (error?.name !== 'TimeoutError' && this?.log?.debug) {
|
|
884
|
+
this.log.debug('Protobuf API had an error performing trait observe. Will restart observe');
|
|
885
|
+
}
|
|
886
|
+
})
|
|
887
|
+
.finally(() => {
|
|
888
|
+
setTimeout(this.#subscribeProtobuf.bind(this, connectionUUID), 1000);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
935
891
|
}
|
|
936
892
|
|
|
937
893
|
async #processPostSubscribe(deviceChanges) {
|
|
@@ -966,27 +922,37 @@ export default class NestAccfactory {
|
|
|
966
922
|
// Device isn't marked as excluded, so create the required HomeKit accessories based upon the device data
|
|
967
923
|
if (deviceData.device_type === NestAccfactory.DeviceType.THERMOSTAT && typeof NestThermostat === 'function') {
|
|
968
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);
|
|
969
927
|
let tempDevice = new NestThermostat(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
970
928
|
tempDevice.add('Nest Thermostat', 9, true);
|
|
971
929
|
}
|
|
972
930
|
|
|
973
931
|
if (deviceData.device_type === NestAccfactory.DeviceType.TEMPSENSOR && typeof NestTemperatureSensor === 'function') {
|
|
974
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);
|
|
975
935
|
let tempDevice = new NestTemperatureSensor(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
976
936
|
tempDevice.add('Nest Temperature Sensor', 10, true);
|
|
977
937
|
}
|
|
978
938
|
|
|
979
939
|
if (deviceData.device_type === NestAccfactory.DeviceType.SMOKESENSOR && typeof NestProtect === 'function') {
|
|
980
940
|
// Nest Protect(s) - Categories.SENSOR = 10
|
|
941
|
+
this?.log?.debug &&
|
|
942
|
+
this.log.debug('Using "%s" data source for "%s"', this.#rawData[object.object_key]?.source, deviceData.description);
|
|
981
943
|
let tempDevice = new NestProtect(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
982
944
|
tempDevice.add('Nest Protect', 10, true);
|
|
983
945
|
}
|
|
984
946
|
|
|
985
947
|
if (
|
|
986
948
|
(deviceData.device_type === NestAccfactory.DeviceType.CAMERA ||
|
|
987
|
-
deviceData.device_type === NestAccfactory.DeviceType.DOORBELL
|
|
988
|
-
|
|
949
|
+
deviceData.device_type === NestAccfactory.DeviceType.DOORBELL ||
|
|
950
|
+
deviceData.device_type === NestAccfactory.DeviceType.FLOODLIGHT) &&
|
|
951
|
+
(typeof NestCamera === 'function' || typeof NestDoorbell === 'function' || typeof NestFloodlight === 'function')
|
|
989
952
|
) {
|
|
953
|
+
this?.log?.debug &&
|
|
954
|
+
this.log.debug('Using "%s" data source for "%s"', this.#rawData[object.object_key]?.source, deviceData.description);
|
|
955
|
+
|
|
990
956
|
let accessoryName = 'Nest ' + deviceData.model.replace(/\s*(?:\([^()]*\))/gi, '');
|
|
991
957
|
if (deviceData.device_type === NestAccfactory.DeviceType.CAMERA) {
|
|
992
958
|
// Nest Camera(s) - Categories.IP_CAMERA = 17
|
|
@@ -998,39 +964,41 @@ export default class NestAccfactory {
|
|
|
998
964
|
let tempDevice = new NestDoorbell(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
999
965
|
tempDevice.add(accessoryName, 18, true);
|
|
1000
966
|
}
|
|
967
|
+
if (deviceData.device_type === NestAccfactory.DeviceType.FLOODLIGHT) {
|
|
968
|
+
// Nest Camera(s) with Floodlight - Categories.IP_CAMERA = 17
|
|
969
|
+
let tempDevice = new NestFloodlight(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
970
|
+
tempDevice.add(accessoryName, 17, true);
|
|
971
|
+
}
|
|
1001
972
|
|
|
1002
973
|
// Setup polling loop for camera/doorbell zone data if not already created.
|
|
1003
|
-
// This is only required for REST API data sources as these details are present in
|
|
974
|
+
// This is only required for REST API data sources as these details are present in Protobuf API
|
|
1004
975
|
if (
|
|
1005
|
-
|
|
976
|
+
this.#rawData?.[object.object_key] !== undefined &&
|
|
977
|
+
this.#rawData[object.object_key]?.timers?.zones === undefined &&
|
|
1006
978
|
this.#rawData[object.object_key].source === NestAccfactory.DataSource.REST
|
|
1007
979
|
) {
|
|
1008
980
|
this.#rawData[object.object_key].timers.zones = setInterval(async () => {
|
|
1009
|
-
if (
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
this.#rawData[object.object_key].value.nexus_api_http_server_url +
|
|
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 +
|
|
1014
985
|
'/cuepoint_category/' +
|
|
1015
986
|
object.object_key.split('.')[1],
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
this.#connections[this.#rawData[object.object_key].connection].cameraAPI.
|
|
1021
|
-
|
|
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,
|
|
1022
996
|
},
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
await axios(request)
|
|
1027
|
-
.then((response) => {
|
|
1028
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
1029
|
-
throw new Error('REST API had error retrieving camera/doorbell activity zones');
|
|
1030
|
-
}
|
|
1031
|
-
|
|
997
|
+
)
|
|
998
|
+
.then((response) => response.json())
|
|
999
|
+
.then((data) => {
|
|
1032
1000
|
let zones = [];
|
|
1033
|
-
|
|
1001
|
+
data.forEach((zone) => {
|
|
1034
1002
|
if (zone.type.toUpperCase() === 'ACTIVITY' || zone.type.toUpperCase() === 'REGION') {
|
|
1035
1003
|
zones.push({
|
|
1036
1004
|
id: zone.id === 0 ? 1 : zone.id,
|
|
@@ -1050,13 +1018,12 @@ export default class NestAccfactory {
|
|
|
1050
1018
|
})
|
|
1051
1019
|
.catch((error) => {
|
|
1052
1020
|
// Log debug message if wasn't a timeout
|
|
1053
|
-
if (error?.
|
|
1054
|
-
this
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
);
|
|
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
|
+
);
|
|
1060
1027
|
}
|
|
1061
1028
|
});
|
|
1062
1029
|
}
|
|
@@ -1064,7 +1031,7 @@ export default class NestAccfactory {
|
|
|
1064
1031
|
}
|
|
1065
1032
|
|
|
1066
1033
|
// Setup polling loop for camera/doorbell alert data if not already created
|
|
1067
|
-
if (
|
|
1034
|
+
if (this.#rawData?.[object.object_key] !== undefined && this.#rawData?.[object.object_key]?.timers?.alerts === undefined) {
|
|
1068
1035
|
this.#rawData[object.object_key].timers.alerts = setInterval(async () => {
|
|
1069
1036
|
if (
|
|
1070
1037
|
typeof this.#rawData[object.object_key]?.value === 'object' &&
|
|
@@ -1072,28 +1039,40 @@ export default class NestAccfactory {
|
|
|
1072
1039
|
) {
|
|
1073
1040
|
let alerts = []; // No alerts yet
|
|
1074
1041
|
|
|
1075
|
-
let commandResponse = await this.#protobufCommand(
|
|
1042
|
+
let commandResponse = await this.#protobufCommand(
|
|
1043
|
+
this.#rawData[object.object_key].connection,
|
|
1044
|
+
'ResourceApi',
|
|
1045
|
+
'SendCommand',
|
|
1076
1046
|
{
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
+
},
|
|
1086
1065
|
},
|
|
1087
1066
|
},
|
|
1088
|
-
|
|
1067
|
+
],
|
|
1089
1068
|
},
|
|
1090
|
-
|
|
1069
|
+
);
|
|
1091
1070
|
|
|
1092
1071
|
if (
|
|
1093
|
-
typeof commandResponse?.
|
|
1072
|
+
typeof commandResponse?.sendCommandResponse?.[0]?.traitOperations?.[0]?.event?.event?.cameraEventWindow
|
|
1094
1073
|
?.cameraEvent === 'object'
|
|
1095
1074
|
) {
|
|
1096
|
-
commandResponse.
|
|
1075
|
+
commandResponse.sendCommandResponse[0].traitOperations[0].event.event.cameraEventWindow.cameraEvent.forEach(
|
|
1097
1076
|
(event) => {
|
|
1098
1077
|
alerts.push({
|
|
1099
1078
|
playback_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
|
|
@@ -1115,7 +1094,7 @@ export default class NestAccfactory {
|
|
|
1115
1094
|
// 'EVENT_UNFAMILIAR_FACE' = 'unfamiliar-face'
|
|
1116
1095
|
// 'EVENT_PERSON_TALKING' = 'personHeard'
|
|
1117
1096
|
// 'EVENT_DOG_BARKING' = 'dogBarking'
|
|
1118
|
-
// <---- TODO (as the ones we use match from
|
|
1097
|
+
// <---- TODO (as the ones we use match from Protobuf)
|
|
1119
1098
|
},
|
|
1120
1099
|
);
|
|
1121
1100
|
|
|
@@ -1140,31 +1119,27 @@ export default class NestAccfactory {
|
|
|
1140
1119
|
this.#rawData[object.object_key]?.source === NestAccfactory.DataSource.REST
|
|
1141
1120
|
) {
|
|
1142
1121
|
let alerts = []; // No alerts yet
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
this.#rawData[object.object_key].value.nexus_api_http_server_url +
|
|
1122
|
+
await fetchWrapper(
|
|
1123
|
+
'get',
|
|
1124
|
+
this.#rawData[object.object_key].value.nexus_api_http_server_url +
|
|
1147
1125
|
'/cuepoint/' +
|
|
1148
1126
|
object.object_key.split('.')[1] +
|
|
1149
1127
|
'/2?start_time=' +
|
|
1150
1128
|
Math.floor(Date.now() / 1000 - 30),
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
this.#connections[this.#rawData[object.object_key].connection].cameraAPI.
|
|
1156
|
-
|
|
1129
|
+
{
|
|
1130
|
+
headers: {
|
|
1131
|
+
referer: 'https://' + this.#connections[this.#rawData[object.object_key].connection].referer,
|
|
1132
|
+
'User-Agent': USERAGENT,
|
|
1133
|
+
[this.#connections[this.#rawData[object.object_key].connection].cameraAPI.key]:
|
|
1134
|
+
this.#connections[this.#rawData[object.object_key].connection].cameraAPI.value +
|
|
1135
|
+
this.#connections[this.#rawData[object.object_key].connection].cameraAPI.token,
|
|
1136
|
+
},
|
|
1137
|
+
timeout: CAMERAALERTPOLLING,
|
|
1157
1138
|
},
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
.then((response) => {
|
|
1163
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
1164
|
-
throw new Error('REST API had error retrieving camera/doorbell activity notifications');
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
response.data.forEach((alert) => {
|
|
1139
|
+
)
|
|
1140
|
+
.then((response) => response.json())
|
|
1141
|
+
.then((data) => {
|
|
1142
|
+
data.forEach((alert) => {
|
|
1168
1143
|
// Fix up alert zone IDs. If there is an ID of 0, we'll transform to 1. ie: main zone
|
|
1169
1144
|
// If there are NO zone IDs, we'll put a 1 in there ie: main zone
|
|
1170
1145
|
alert.zone_ids = alert.zone_ids.map((id) => (id !== 0 ? id : 1));
|
|
@@ -1190,13 +1165,12 @@ export default class NestAccfactory {
|
|
|
1190
1165
|
})
|
|
1191
1166
|
.catch((error) => {
|
|
1192
1167
|
// Log debug message if wasn't a timeout
|
|
1193
|
-
if (error?.
|
|
1194
|
-
this
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
);
|
|
1168
|
+
if (error?.name !== 'TimeoutError' && this?.log?.debug) {
|
|
1169
|
+
this.log.debug(
|
|
1170
|
+
'REST API had error retrieving camera/doorbell activity notifications for uuid "%s". Error was "%s"',
|
|
1171
|
+
object.object_key,
|
|
1172
|
+
error?.code,
|
|
1173
|
+
);
|
|
1200
1174
|
}
|
|
1201
1175
|
});
|
|
1202
1176
|
|
|
@@ -1212,6 +1186,8 @@ export default class NestAccfactory {
|
|
|
1212
1186
|
}
|
|
1213
1187
|
if (deviceData.device_type === NestAccfactory.DeviceType.WEATHER && typeof NestWeather === 'function') {
|
|
1214
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);
|
|
1215
1191
|
let tempDevice = new NestWeather(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
1216
1192
|
tempDevice.add('Nest Weather', 10, true);
|
|
1217
1193
|
|
|
@@ -1251,12 +1227,12 @@ export default class NestAccfactory {
|
|
|
1251
1227
|
let devices = {};
|
|
1252
1228
|
|
|
1253
1229
|
// Get the device(s) location from stucture
|
|
1254
|
-
// We'll test in both REST and
|
|
1230
|
+
// We'll test in both REST and Protobuf API data
|
|
1255
1231
|
const get_location_name = (structure_id, where_id) => {
|
|
1256
1232
|
let location = '';
|
|
1257
1233
|
|
|
1258
1234
|
// Check REST data
|
|
1259
|
-
if (typeof this.#rawData['where.' + structure_id]?.value === 'object') {
|
|
1235
|
+
if (typeof this.#rawData?.['where.' + structure_id]?.value === 'object') {
|
|
1260
1236
|
this.#rawData['where.' + structure_id].value.wheres.forEach((value) => {
|
|
1261
1237
|
if (where_id === value.where_id) {
|
|
1262
1238
|
location = value.name;
|
|
@@ -1264,7 +1240,7 @@ export default class NestAccfactory {
|
|
|
1264
1240
|
});
|
|
1265
1241
|
}
|
|
1266
1242
|
|
|
1267
|
-
// Check
|
|
1243
|
+
// Check Protobuf data
|
|
1268
1244
|
if (typeof this.#rawData[structure_id]?.value?.located_annotations?.predefinedWheres === 'object') {
|
|
1269
1245
|
Object.values(this.#rawData[structure_id].value.located_annotations.predefinedWheres).forEach((value) => {
|
|
1270
1246
|
if (value.whereId.resourceId === where_id) {
|
|
@@ -1358,7 +1334,7 @@ export default class NestAccfactory {
|
|
|
1358
1334
|
.forEach(([object_key, value]) => {
|
|
1359
1335
|
let tempDevice = {};
|
|
1360
1336
|
try {
|
|
1361
|
-
if (value
|
|
1337
|
+
if (value?.source === NestAccfactory.DataSource.PROTOBUF) {
|
|
1362
1338
|
let RESTTypeData = {};
|
|
1363
1339
|
RESTTypeData.mac_address = Buffer.from(value.value.wifi_interface.macAddress, 'base64');
|
|
1364
1340
|
RESTTypeData.serial_number = value.value.device_identity.serialNumber;
|
|
@@ -1398,59 +1374,57 @@ export default class NestAccfactory {
|
|
|
1398
1374
|
? true
|
|
1399
1375
|
: false;
|
|
1400
1376
|
RESTTypeData.can_cool =
|
|
1401
|
-
value.value
|
|
1402
|
-
value.value
|
|
1403
|
-
value.value
|
|
1377
|
+
value.value?.hvac_equipment_capabilities?.hasStage1Cool === true ||
|
|
1378
|
+
value.value?.hvac_equipment_capabilities?.hasStage2Cool === true ||
|
|
1379
|
+
value.value?.hvac_equipment_capabilities?.hasStage3Cool === true;
|
|
1404
1380
|
RESTTypeData.can_heat =
|
|
1405
|
-
value.value
|
|
1406
|
-
value.value
|
|
1407
|
-
value.value
|
|
1408
|
-
RESTTypeData.temperature_lock = value.value
|
|
1381
|
+
value.value?.hvac_equipment_capabilities?.hasStage1Heat === true ||
|
|
1382
|
+
value.value?.hvac_equipment_capabilities?.hasStage2Heat === true ||
|
|
1383
|
+
value.value?.hvac_equipment_capabilities?.hasStage3Heat === true;
|
|
1384
|
+
RESTTypeData.temperature_lock = value.value?.temperature_lock_settings?.enabled === true;
|
|
1409
1385
|
RESTTypeData.temperature_lock_pin_hash =
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
RESTTypeData.away = value.value.structure_mode.structureMode === 'STRUCTURE_MODE_AWAY';
|
|
1414
|
-
RESTTypeData.occupancy = value.value.structure_mode.structureMode === 'STRUCTURE_MODE_HOME';
|
|
1386
|
+
value.value?.temperature_lock_settings?.enabled === true ? value.value.temperature_lock_settings.pinHash : '';
|
|
1387
|
+
RESTTypeData.away = value.value?.structure_mode?.structureMode === 'STRUCTURE_MODE_AWAY';
|
|
1388
|
+
RESTTypeData.occupancy = value.value?.structure_mode?.structureMode === 'STRUCTURE_MODE_HOME';
|
|
1415
1389
|
//RESTTypeData.occupancy = (value.value.structure_mode.occupancy.activity === 'ACTIVITY_ACTIVE');
|
|
1416
|
-
RESTTypeData.vacation_mode = value.value
|
|
1417
|
-
RESTTypeData.description =
|
|
1390
|
+
RESTTypeData.vacation_mode = value.value?.structure_mode?.structureMode === 'STRUCTURE_MODE_VACATION';
|
|
1391
|
+
RESTTypeData.description = value.value.label?.label !== undefined ? value.value.label.label : '';
|
|
1418
1392
|
RESTTypeData.location = get_location_name(
|
|
1419
|
-
value.value
|
|
1420
|
-
value.value
|
|
1393
|
+
value.value?.device_info?.pairerId?.resourceId,
|
|
1394
|
+
value.value?.device_located_settings?.whereAnnotationRid?.resourceId,
|
|
1421
1395
|
);
|
|
1422
1396
|
|
|
1423
1397
|
// Work out current mode. ie: off, cool, heat, range and get temperature low/high and target
|
|
1424
1398
|
RESTTypeData.hvac_mode =
|
|
1425
|
-
value.value
|
|
1399
|
+
value.value?.target_temperature_settings?.enabled?.value === true &&
|
|
1400
|
+
value.value?.target_temperature_settings?.targetTemperature?.setpointType !== undefined
|
|
1426
1401
|
? value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase()
|
|
1427
1402
|
: 'off';
|
|
1428
1403
|
RESTTypeData.target_temperature_low =
|
|
1429
|
-
typeof value.value
|
|
1404
|
+
typeof value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value === 'number'
|
|
1430
1405
|
? value.value.target_temperature_settings.targetTemperature.heatingTarget.value
|
|
1431
1406
|
: 0.0;
|
|
1432
1407
|
RESTTypeData.target_temperature_high =
|
|
1433
|
-
typeof value.value
|
|
1408
|
+
typeof value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value === 'number'
|
|
1434
1409
|
? value.value.target_temperature_settings.targetTemperature.coolingTarget.value
|
|
1435
1410
|
: 0.0;
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
}
|
|
1411
|
+
RESTTypeData.target_temperature =
|
|
1412
|
+
value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'SET_POINT_TYPE_COOL' &&
|
|
1413
|
+
typeof value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value === 'number'
|
|
1414
|
+
? value.value.target_temperature_settings.targetTemperature.coolingTarget.value
|
|
1415
|
+
: value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'SET_POINT_TYPE_HEAT' &&
|
|
1416
|
+
typeof value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value === 'number'
|
|
1417
|
+
? value.value.target_temperature_settings.targetTemperature.heatingTarget.value
|
|
1418
|
+
: value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'SET_POINT_TYPE_RANGE' &&
|
|
1419
|
+
typeof value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value === 'number' &&
|
|
1420
|
+
typeof value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value === 'number'
|
|
1421
|
+
? (value.value.target_temperature_settings.targetTemperature.coolingTarget.value +
|
|
1422
|
+
value.value.target_temperature_settings.targetTemperature.heatingTarget.value) *
|
|
1423
|
+
0.5
|
|
1424
|
+
: 0.0;
|
|
1451
1425
|
|
|
1452
1426
|
// Work out if eco mode is active and adjust temperature low/high and target
|
|
1453
|
-
if (value.value
|
|
1427
|
+
if (value.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE') {
|
|
1454
1428
|
RESTTypeData.target_temperature_low = value.value.eco_mode_settings.ecoTemperatureHeat.value.value;
|
|
1455
1429
|
RESTTypeData.target_temperature_high = value.value.eco_mode_settings.ecoTemperatureCool.value.value;
|
|
1456
1430
|
if (
|
|
@@ -1482,21 +1456,21 @@ export default class NestAccfactory {
|
|
|
1482
1456
|
// Work out current state ie: heating, cooling etc
|
|
1483
1457
|
RESTTypeData.hvac_state = 'off'; // By default, we're not heating or cooling
|
|
1484
1458
|
if (
|
|
1485
|
-
value.value
|
|
1486
|
-
value.value
|
|
1487
|
-
value.value
|
|
1459
|
+
value.value?.hvac_control?.hvacState?.coolStage1Active === true ||
|
|
1460
|
+
value.value?.hvac_control?.hvacState?.coolStage2Active === true ||
|
|
1461
|
+
value.value?.hvac_control?.hvacState?.coolStage2Active === true
|
|
1488
1462
|
) {
|
|
1489
1463
|
// A cooling source is on, so we're in cooling mode
|
|
1490
1464
|
RESTTypeData.hvac_state = 'cooling';
|
|
1491
1465
|
}
|
|
1492
1466
|
if (
|
|
1493
|
-
value.value
|
|
1494
|
-
value.value
|
|
1495
|
-
value.value
|
|
1496
|
-
value.value
|
|
1497
|
-
value.value
|
|
1498
|
-
value.value
|
|
1499
|
-
value.value
|
|
1467
|
+
value.value?.hvac_control?.hvacState?.heatStage1Active === true ||
|
|
1468
|
+
value.value?.hvac_control?.hvacState?.heatStage2Active === true ||
|
|
1469
|
+
value.value?.hvac_control?.hvacState?.heatStage3Active === true ||
|
|
1470
|
+
value.value?.hvac_control?.hvacState?.alternateHeatStage1Active === true ||
|
|
1471
|
+
value.value?.hvac_control?.hvacState?.alternateHeatStage2Active === true ||
|
|
1472
|
+
value.value?.hvac_control?.hvacState?.auxiliaryHeatActive === true ||
|
|
1473
|
+
value.value?.hvac_control?.hvacState?.emergencyHeatActive === true
|
|
1500
1474
|
) {
|
|
1501
1475
|
// A heating source is on, so we're in heating mode
|
|
1502
1476
|
RESTTypeData.hvac_state = 'heating';
|
|
@@ -1524,20 +1498,20 @@ export default class NestAccfactory {
|
|
|
1524
1498
|
|
|
1525
1499
|
// Process any temperature sensors associated with this thermostat
|
|
1526
1500
|
RESTTypeData.active_rcs_sensor =
|
|
1527
|
-
|
|
1501
|
+
value.value.remote_comfort_sensing_settings?.activeRcsSelection?.activeRcsSensor !== undefined
|
|
1528
1502
|
? value.value.remote_comfort_sensing_settings.activeRcsSelection.activeRcsSensor.resourceId
|
|
1529
1503
|
: '';
|
|
1530
1504
|
RESTTypeData.linked_rcs_sensors = [];
|
|
1531
|
-
if (
|
|
1505
|
+
if (value.value?.remote_comfort_sensing_settings?.associatedRcsSensors !== undefined) {
|
|
1532
1506
|
value.value.remote_comfort_sensing_settings.associatedRcsSensors.forEach((sensor) => {
|
|
1533
|
-
if (
|
|
1507
|
+
if (this.#rawData?.[sensor.deviceId.resourceId]?.value !== undefined) {
|
|
1534
1508
|
this.#rawData[sensor.deviceId.resourceId].value.associated_thermostat = object_key; // Sensor is linked to this thermostat
|
|
1535
1509
|
|
|
1536
1510
|
// Get sensor online/offline status
|
|
1537
|
-
// 'liveness' property doesn't appear in
|
|
1511
|
+
// 'liveness' property doesn't appear in Protobuf data for temp sensors, so we'll add that object here
|
|
1538
1512
|
this.#rawData[sensor.deviceId.resourceId].value.liveness = {};
|
|
1539
1513
|
this.#rawData[sensor.deviceId.resourceId].value.liveness.status = 'LIVENESS_DEVICE_STATUS_UNSPECIFIED';
|
|
1540
|
-
if (
|
|
1514
|
+
if (value.value?.remote_comfort_sensing_state?.rcsSensorStatuses !== undefined) {
|
|
1541
1515
|
Object.values(value.value.remote_comfort_sensing_state.rcsSensorStatuses).forEach((sensorStatus) => {
|
|
1542
1516
|
if (
|
|
1543
1517
|
sensorStatus?.sensorId?.resourceId === sensor.deviceId.resourceId &&
|
|
@@ -1554,14 +1528,14 @@ export default class NestAccfactory {
|
|
|
1554
1528
|
}
|
|
1555
1529
|
|
|
1556
1530
|
RESTTypeData.schedule_mode =
|
|
1531
|
+
value.value?.target_temperature_settings?.targetTemperature?.setpointType !== undefined &&
|
|
1557
1532
|
value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase() !== 'off'
|
|
1558
1533
|
? value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase()
|
|
1559
1534
|
: '';
|
|
1560
1535
|
RESTTypeData.schedules = {};
|
|
1561
|
-
|
|
1562
1536
|
if (
|
|
1563
|
-
|
|
1564
|
-
value.value[RESTTypeData.schedule_mode + '_schedule_settings']
|
|
1537
|
+
value.value[RESTTypeData.schedule_mode + '_schedule_settings']?.setpoints !== undefined &&
|
|
1538
|
+
value.value[RESTTypeData.schedule_mode + '_schedule_settings']?.type ===
|
|
1565
1539
|
'SET_POINT_SCHEDULE_TYPE_' + RESTTypeData.schedule_mode.toUpperCase()
|
|
1566
1540
|
) {
|
|
1567
1541
|
Object.values(value.value[RESTTypeData.schedule_mode + '_schedule_settings'].setpoints).forEach((schedule) => {
|
|
@@ -1586,7 +1560,7 @@ export default class NestAccfactory {
|
|
|
1586
1560
|
tempDevice = process_thermostat_data(object_key, RESTTypeData);
|
|
1587
1561
|
}
|
|
1588
1562
|
|
|
1589
|
-
if (value
|
|
1563
|
+
if (value?.source === NestAccfactory.DataSource.REST) {
|
|
1590
1564
|
let RESTTypeData = {};
|
|
1591
1565
|
RESTTypeData.mac_address = value.value.mac_address;
|
|
1592
1566
|
RESTTypeData.serial_number = value.value.serial_number;
|
|
@@ -1610,68 +1584,54 @@ export default class NestAccfactory {
|
|
|
1610
1584
|
RESTTypeData.backplate_temperature = value.value.backplate_temperature;
|
|
1611
1585
|
RESTTypeData.current_temperature = value.value.backplate_temperature;
|
|
1612
1586
|
RESTTypeData.battery_level = value.value.battery_level;
|
|
1613
|
-
RESTTypeData.online = this.#rawData['track.' + value.value.serial_number]
|
|
1587
|
+
RESTTypeData.online = this.#rawData?.['track.' + value.value.serial_number]?.value?.online === true;
|
|
1614
1588
|
RESTTypeData.leaf = value.value.leaf === true;
|
|
1615
1589
|
RESTTypeData.has_humidifier = value.value.has_humidifier === true;
|
|
1616
1590
|
RESTTypeData.has_dehumidifier = value.value.has_dehumidifier === true;
|
|
1617
1591
|
RESTTypeData.has_fan = value.value.has_fan === true;
|
|
1618
|
-
RESTTypeData.can_cool = this.#rawData['shared.' + value.value.serial_number]
|
|
1619
|
-
RESTTypeData.can_heat = this.#rawData['shared.' + value.value.serial_number]
|
|
1592
|
+
RESTTypeData.can_cool = this.#rawData?.['shared.' + value.value.serial_number]?.value?.can_cool === true;
|
|
1593
|
+
RESTTypeData.can_heat = this.#rawData?.['shared.' + value.value.serial_number]?.value?.can_heat === true;
|
|
1620
1594
|
RESTTypeData.temperature_lock = value.value.temperature_lock === true;
|
|
1621
1595
|
RESTTypeData.temperature_lock_pin_hash = value.value.temperature_lock_pin_hash;
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
if (
|
|
1631
|
-
this.#rawData['structure.' + this.#rawData['link.' + value.value.serial_number].value.structure.split('.')[1]]?.value
|
|
1632
|
-
?.structure_mode?.structureMode === 'STRUCTURE_MODE_AWAY'
|
|
1633
|
-
) {
|
|
1634
|
-
RESTTypeData.away = true;
|
|
1635
|
-
}
|
|
1596
|
+
|
|
1597
|
+
// Look in two possible locations for away status
|
|
1598
|
+
RESTTypeData.away =
|
|
1599
|
+
this.#rawData?.['structure.' + this.#rawData?.['link.' + value.value.serial_number].value.structure.split('.')[1]]?.value
|
|
1600
|
+
?.away === true ||
|
|
1601
|
+
this.#rawData?.['structure.' + this.#rawData?.['link.' + value.value.serial_number].value.structure.split('.')[1]]?.value
|
|
1602
|
+
?.structure_mode?.structureMode === 'STRUCTURE_MODE_AWAY';
|
|
1603
|
+
|
|
1636
1604
|
RESTTypeData.occupancy = RESTTypeData.away === false; // Occupancy is opposite of away status ie: away is false, then occupied
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
].value.vacation_mode; // vacation mode
|
|
1646
|
-
}
|
|
1647
|
-
if (
|
|
1648
|
-
this.#rawData['structure.' + this.#rawData['link.' + value.value.serial_number].value.structure.split('.')[1]]?.value
|
|
1649
|
-
?.structure_mode?.structureMode === 'STRUCTURE_MODE_VACATION'
|
|
1650
|
-
) {
|
|
1651
|
-
RESTTypeData.vacation_mode = true;
|
|
1652
|
-
}
|
|
1605
|
+
|
|
1606
|
+
// Look in two possible locations for vacation status
|
|
1607
|
+
RESTTypeData.vacation_mode =
|
|
1608
|
+
this.#rawData['structure.' + this.#rawData?.['link.' + value.value.serial_number].value.structure.split('.')[1]]?.value
|
|
1609
|
+
?.vacation_mode === true ||
|
|
1610
|
+
this.#rawData?.['structure.' + this.#rawData?.['link.' + value.value.serial_number].value.structure.split('.')[1]]?.value
|
|
1611
|
+
?.structure_mode?.structureMode === 'STRUCTURE_MODE_VACATION';
|
|
1612
|
+
|
|
1653
1613
|
RESTTypeData.description =
|
|
1654
|
-
|
|
1614
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.name !== undefined
|
|
1655
1615
|
? makeHomeKitName(this.#rawData['shared.' + value.value.serial_number].value.name)
|
|
1656
1616
|
: '';
|
|
1657
1617
|
RESTTypeData.location = get_location_name(
|
|
1658
|
-
this.#rawData['link.' + value.value.serial_number].value.structure.split('.')[1],
|
|
1618
|
+
this.#rawData?.['link.' + value.value.serial_number].value.structure.split('.')[1],
|
|
1659
1619
|
value.value.where_id,
|
|
1660
1620
|
);
|
|
1661
1621
|
|
|
1662
1622
|
// Work out current mode. ie: off, cool, heat, range and get temperature low (heat) and high (cool)
|
|
1663
|
-
RESTTypeData.hvac_mode = this.#rawData['shared.' + value.value.serial_number].value.target_temperature_type;
|
|
1664
|
-
RESTTypeData.target_temperature_low = this.#rawData['shared.' + value.value.serial_number].value.target_temperature_low;
|
|
1665
|
-
RESTTypeData.target_temperature_high = this.#rawData['shared.' + value.value.serial_number].value.target_temperature_high;
|
|
1666
|
-
if (this.#rawData['shared.' + value.value.serial_number]
|
|
1623
|
+
RESTTypeData.hvac_mode = this.#rawData?.['shared.' + value.value.serial_number].value.target_temperature_type;
|
|
1624
|
+
RESTTypeData.target_temperature_low = this.#rawData?.['shared.' + value.value.serial_number].value.target_temperature_low;
|
|
1625
|
+
RESTTypeData.target_temperature_high = this.#rawData?.['shared.' + value.value.serial_number].value.target_temperature_high;
|
|
1626
|
+
if (this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type.toUpperCase() === 'COOL') {
|
|
1667
1627
|
// Target temperature is the cooling point
|
|
1668
1628
|
RESTTypeData.target_temperature = this.#rawData['shared.' + value.value.serial_number].value.target_temperature_high;
|
|
1669
1629
|
}
|
|
1670
|
-
if (this.#rawData['shared.' + value.value.serial_number]
|
|
1630
|
+
if (this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type.toUpperCase() === 'HEAT') {
|
|
1671
1631
|
// Target temperature is the heating point
|
|
1672
1632
|
RESTTypeData.target_temperature = this.#rawData['shared.' + value.value.serial_number].value.target_temperature_low;
|
|
1673
1633
|
}
|
|
1674
|
-
if (this.#rawData['shared.' + value.value.serial_number]
|
|
1634
|
+
if (this.#rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type.toUpperCase() === 'RANGE') {
|
|
1675
1635
|
// Target temperature is in between the heating and cooling point
|
|
1676
1636
|
RESTTypeData.target_temperature =
|
|
1677
1637
|
(this.#rawData['shared.' + value.value.serial_number].value.target_temperature_low +
|
|
@@ -1700,21 +1660,21 @@ export default class NestAccfactory {
|
|
|
1700
1660
|
// Work out current state ie: heating, cooling etc
|
|
1701
1661
|
RESTTypeData.hvac_state = 'off'; // By default, we're not heating or cooling
|
|
1702
1662
|
if (
|
|
1703
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1704
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1705
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1706
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1707
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1708
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1709
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1663
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_heater_state === true ||
|
|
1664
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_heat_x2_state === true ||
|
|
1665
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_heat_x3_state === true ||
|
|
1666
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_aux_heater_state === true ||
|
|
1667
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_alt_heat_x2_state === true ||
|
|
1668
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_emer_heat_state === true ||
|
|
1669
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_alt_heat_state === true
|
|
1710
1670
|
) {
|
|
1711
1671
|
// A heating source is on, so we're in heating mode
|
|
1712
1672
|
RESTTypeData.hvac_state = 'heating';
|
|
1713
1673
|
}
|
|
1714
1674
|
if (
|
|
1715
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1716
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1717
|
-
this.#rawData['shared.' + value.value.serial_number]
|
|
1675
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_ac_state === true ||
|
|
1676
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_cool_x2_state === true ||
|
|
1677
|
+
this.#rawData?.['shared.' + value.value.serial_number]?.value?.hvac_cool_x3_state === true
|
|
1718
1678
|
) {
|
|
1719
1679
|
// A cooling source is on, so we're in cooling mode
|
|
1720
1680
|
RESTTypeData.hvac_state = 'cooling';
|
|
@@ -1739,21 +1699,26 @@ export default class NestAccfactory {
|
|
|
1739
1699
|
// Process any temperature sensors associated with this thermostat
|
|
1740
1700
|
RESTTypeData.active_rcs_sensor = '';
|
|
1741
1701
|
RESTTypeData.linked_rcs_sensors = [];
|
|
1742
|
-
this.#rawData['rcs_settings.' + value.value.serial_number]
|
|
1743
|
-
|
|
1744
|
-
this.#rawData[sensor]
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1702
|
+
if (this.#rawData?.['rcs_settings.' + value.value.serial_number]?.value?.associated_rcs_sensors !== undefined) {
|
|
1703
|
+
this.#rawData?.['rcs_settings.' + value.value.serial_number].value.associated_rcs_sensors.forEach((sensor) => {
|
|
1704
|
+
if (typeof this.#rawData[sensor]?.value === 'object') {
|
|
1705
|
+
this.#rawData[sensor].value.associated_thermostat = object_key; // Sensor is linked to this thermostat
|
|
1706
|
+
|
|
1707
|
+
// Is this sensor the active one? If so, get some details about it
|
|
1708
|
+
if (
|
|
1709
|
+
this.#rawData?.['rcs_settings.' + value.value.serial_number]?.value?.active_rcs_sensors !== undefined &&
|
|
1710
|
+
this.#rawData?.['rcs_settings.' + value.value.serial_number]?.value?.active_rcs_sensors.includes(sensor)
|
|
1711
|
+
) {
|
|
1712
|
+
RESTTypeData.active_rcs_sensor = this.#rawData[sensor].value.serial_number.toUpperCase();
|
|
1713
|
+
RESTTypeData.current_temperature = this.#rawData[sensor].value.current_temperature;
|
|
1714
|
+
}
|
|
1715
|
+
RESTTypeData.linked_rcs_sensors.push(this.#rawData[sensor].value.serial_number.toUpperCase());
|
|
1750
1716
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
});
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1754
1719
|
|
|
1755
1720
|
// Get associated schedules
|
|
1756
|
-
if (
|
|
1721
|
+
if (this.#rawData?.['schedule.' + value.value.serial_number] !== undefined) {
|
|
1757
1722
|
Object.values(this.#rawData['schedule.' + value.value.serial_number].value.days).forEach((schedules) => {
|
|
1758
1723
|
Object.values(schedules).forEach((schedule) => {
|
|
1759
1724
|
// Fix up temperatures in the schedule
|
|
@@ -1872,7 +1837,7 @@ export default class NestAccfactory {
|
|
|
1872
1837
|
let tempDevice = {};
|
|
1873
1838
|
try {
|
|
1874
1839
|
if (
|
|
1875
|
-
value
|
|
1840
|
+
value?.source === NestAccfactory.DataSource.PROTOBUF &&
|
|
1876
1841
|
typeof value?.value?.associated_thermostat === 'string' &&
|
|
1877
1842
|
value?.value?.associated_thermostat !== ''
|
|
1878
1843
|
) {
|
|
@@ -1881,21 +1846,21 @@ export default class NestAccfactory {
|
|
|
1881
1846
|
// Guessing battery minimum voltage is 2v??
|
|
1882
1847
|
RESTTypeData.battery_level = scaleValue(value.value.battery.assessedVoltage.value, 2.0, 3.0, 0, 100);
|
|
1883
1848
|
RESTTypeData.current_temperature = value.value.current_temperature.temperatureValue.temperature.value;
|
|
1884
|
-
// Online status we 'faked' when processing Thermostat
|
|
1849
|
+
// Online status we 'faked' when processing Thermostat Protobuf data
|
|
1885
1850
|
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
1886
1851
|
RESTTypeData.associated_thermostat = value.value.associated_thermostat;
|
|
1887
1852
|
RESTTypeData.description = typeof value.value?.label?.label === 'string' ? value.value.label.label : '';
|
|
1888
1853
|
RESTTypeData.location = get_location_name(
|
|
1889
|
-
value.value
|
|
1890
|
-
value.value
|
|
1854
|
+
value.value?.device_info?.pairerId?.resourceId,
|
|
1855
|
+
value.value?.device_located_settings?.whereAnnotationRid?.resourceId,
|
|
1891
1856
|
);
|
|
1892
1857
|
RESTTypeData.active_sensor =
|
|
1893
|
-
this.#rawData[value.value
|
|
1894
|
-
?.resourceId === object_key;
|
|
1858
|
+
this.#rawData?.[value.value?.associated_thermostat].value?.remote_comfort_sensing_settings?.activeRcsSelection
|
|
1859
|
+
?.activeRcsSensor?.resourceId === object_key;
|
|
1895
1860
|
tempDevice = process_kryptonite_data(object_key, RESTTypeData);
|
|
1896
1861
|
}
|
|
1897
1862
|
if (
|
|
1898
|
-
value
|
|
1863
|
+
value?.source === NestAccfactory.DataSource.REST &&
|
|
1899
1864
|
typeof value?.value?.associated_thermostat === 'string' &&
|
|
1900
1865
|
value?.value?.associated_thermostat !== ''
|
|
1901
1866
|
) {
|
|
@@ -1908,7 +1873,8 @@ export default class NestAccfactory {
|
|
|
1908
1873
|
RESTTypeData.description = value.value.description;
|
|
1909
1874
|
RESTTypeData.location = get_location_name(value.value.structure_id, value.value.where_id);
|
|
1910
1875
|
RESTTypeData.active_sensor =
|
|
1911
|
-
this.#rawData['rcs_settings.' + value.value
|
|
1876
|
+
this.#rawData?.['rcs_settings.' + value.value?.associated_thermostat]?.value?.active_rcs_sensors.includes(object_key) ===
|
|
1877
|
+
true;
|
|
1912
1878
|
tempDevice = process_kryptonite_data(object_key, RESTTypeData);
|
|
1913
1879
|
}
|
|
1914
1880
|
// eslint-disable-next-line no-unused-vars
|
|
@@ -1999,7 +1965,7 @@ export default class NestAccfactory {
|
|
|
1999
1965
|
.forEach(([object_key, value]) => {
|
|
2000
1966
|
let tempDevice = {};
|
|
2001
1967
|
try {
|
|
2002
|
-
if (value
|
|
1968
|
+
if (value?.source === NestAccfactory.DataSource.PROTOBUF) {
|
|
2003
1969
|
/*
|
|
2004
1970
|
let RESTTypeData = {};
|
|
2005
1971
|
RESTTypeData.mac_address = Buffer.from(value.value.wifi_interface.macAddress, 'base64');
|
|
@@ -2012,11 +1978,11 @@ export default class NestAccfactory {
|
|
|
2012
1978
|
RESTTypeData.battery_health_state = value.value.battery_voltage_bank1.faultInformation;
|
|
2013
1979
|
RESTTypeData.smoke_status = value.value.safety_alarm_smoke.alarmState === 'ALARM_STATE_ALARM' ? 2 : 0; // matches REST data
|
|
2014
1980
|
RESTTypeData.co_status = value.value.safety_alarm_co.alarmState === 'ALARM_STATE_ALARM' ? 2 : 0; // matches REST data
|
|
2015
|
-
RESTTypeData.heat_status =
|
|
1981
|
+
// RESTTypeData.heat_status =
|
|
2016
1982
|
RESTTypeData.hushed_state =
|
|
2017
1983
|
value.value.safety_alarm_smoke.silenceState === 'SILENCE_STATE_SILENCED' ||
|
|
2018
1984
|
value.value.safety_alarm_co.silenceState === 'SILENCE_STATE_SILENCED';
|
|
2019
|
-
RESTTypeData.
|
|
1985
|
+
RESTTypeData.ntp_green_led_enable = value.value.night_time_promise_settings.greenLedEnabled === true;
|
|
2020
1986
|
RESTTypeData.smoke_test_passed = value.value.safety_summary.warningDevices.failures.includes('FAILURE_TYPE_SMOKE') === false;
|
|
2021
1987
|
RESTTypeData.heat_test_passed = value.value.safety_summary.warningDevices.failures.includes('FAILURE_TYPE_TEMP') === false;
|
|
2022
1988
|
RESTTypeData.latest_alarm_test =
|
|
@@ -2029,7 +1995,7 @@ export default class NestAccfactory {
|
|
|
2029
1995
|
? parseInt(value.value.legacy_protect_device_settings.replaceByDate.seconds)
|
|
2030
1996
|
: 0;
|
|
2031
1997
|
|
|
2032
|
-
RESTTypeData.removed_from_base =
|
|
1998
|
+
// RESTTypeData.removed_from_base =
|
|
2033
1999
|
RESTTypeData.topaz_hush_key =
|
|
2034
2000
|
typeof value.value.safety_structure_settings.structureHushKey === 'string'
|
|
2035
2001
|
? value.value.safety_structure_settings.structureHushKey
|
|
@@ -2037,19 +2003,19 @@ export default class NestAccfactory {
|
|
|
2037
2003
|
RESTTypeData.detected_motion = value.value.legacy_protect_device_info.autoAway === false;
|
|
2038
2004
|
RESTTypeData.description = typeof value.value?.label?.label === 'string' ? value.value.label.label : '';
|
|
2039
2005
|
RESTTypeData.location = get_location_name(
|
|
2040
|
-
value.value
|
|
2041
|
-
value.value
|
|
2006
|
+
value.value?.device_info?.pairerId?.resourceId,
|
|
2007
|
+
value.value?.device_located_settings?.whereAnnotationRid?.resourceId,
|
|
2042
2008
|
);
|
|
2043
2009
|
tempDevice = process_protect_data(object_key, RESTTypeData);
|
|
2044
2010
|
*/
|
|
2045
2011
|
}
|
|
2046
2012
|
|
|
2047
|
-
if (value
|
|
2013
|
+
if (value?.source === NestAccfactory.DataSource.REST) {
|
|
2048
2014
|
let RESTTypeData = {};
|
|
2049
2015
|
RESTTypeData.mac_address = value.value.wifi_mac_address;
|
|
2050
2016
|
RESTTypeData.serial_number = value.value.serial_number;
|
|
2051
2017
|
RESTTypeData.software_version = value.value.software_version;
|
|
2052
|
-
RESTTypeData.online = this.#rawData['widget_track.' + value
|
|
2018
|
+
RESTTypeData.online = this.#rawData?.['widget_track.' + value?.value?.thread_mac_address.toUpperCase()]?.value?.online === true;
|
|
2053
2019
|
RESTTypeData.line_power_present = value.value.line_power_present === true;
|
|
2054
2020
|
RESTTypeData.wired_or_battery = value.value.wired_or_battery;
|
|
2055
2021
|
RESTTypeData.battery_level = value.value.battery_level;
|
|
@@ -2058,17 +2024,17 @@ export default class NestAccfactory {
|
|
|
2058
2024
|
RESTTypeData.co_status = value.value.co_status;
|
|
2059
2025
|
RESTTypeData.heat_status = value.value.heat_status;
|
|
2060
2026
|
RESTTypeData.hushed_state = value.value.hushed_state === true;
|
|
2061
|
-
RESTTypeData.
|
|
2027
|
+
RESTTypeData.ntp_green_led_enable = value.value.ntp_green_led_enable === true;
|
|
2062
2028
|
RESTTypeData.smoke_test_passed = value.value.component_smoke_test_passed === true;
|
|
2063
2029
|
RESTTypeData.heat_test_passed = value.value.component_temp_test_passed === true;
|
|
2064
2030
|
RESTTypeData.latest_alarm_test = value.value.latest_manual_test_end_utc_secs;
|
|
2065
2031
|
RESTTypeData.self_test_in_progress =
|
|
2066
|
-
this.#rawData['safety.' + value.value.structure_id]
|
|
2032
|
+
this.#rawData?.['safety.' + value.value.structure_id]?.value?.manual_self_test_in_progress === true;
|
|
2067
2033
|
RESTTypeData.replacement_date = value.value.replace_by_date_utc_secs;
|
|
2068
2034
|
RESTTypeData.removed_from_base = value.value.removed_from_base === true;
|
|
2069
2035
|
RESTTypeData.topaz_hush_key =
|
|
2070
|
-
typeof this.#rawData['structure.' + value.value.structure_id]?.value?.topaz_hush_key === 'string'
|
|
2071
|
-
? this.#rawData['structure.' + value.value.structure_id]
|
|
2036
|
+
typeof this.#rawData?.['structure.' + value.value.structure_id]?.value?.topaz_hush_key === 'string'
|
|
2037
|
+
? this.#rawData?.['structure.' + value.value.structure_id]?.value?.topaz_hush_key
|
|
2072
2038
|
: '';
|
|
2073
2039
|
RESTTypeData.detected_motion = value.value.auto_away === false;
|
|
2074
2040
|
RESTTypeData.description = value.value?.description;
|
|
@@ -2102,6 +2068,9 @@ export default class NestAccfactory {
|
|
|
2102
2068
|
if (data.model.toUpperCase().includes('DOORBELL') === true) {
|
|
2103
2069
|
data.device_type = NestAccfactory.DeviceType.DOORBELL;
|
|
2104
2070
|
}
|
|
2071
|
+
if (data.model.toUpperCase().includes('FLOODLIGHT') === true) {
|
|
2072
|
+
data.device_type = NestAccfactory.DeviceType.FLOODLIGHT;
|
|
2073
|
+
}
|
|
2105
2074
|
data.uuid = object_key; // Internal structure ID
|
|
2106
2075
|
data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
|
|
2107
2076
|
data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
|
|
@@ -2152,7 +2121,6 @@ export default class NestAccfactory {
|
|
|
2152
2121
|
'nest.resource.NestCamIQResource',
|
|
2153
2122
|
'nest.resource.NestCamIQOutdoorResource',
|
|
2154
2123
|
'nest.resource.NestHelloResource',
|
|
2155
|
-
'google.resource.AzizResource',
|
|
2156
2124
|
'google.resource.GoogleNewmanResource',
|
|
2157
2125
|
];
|
|
2158
2126
|
Object.entries(this.#rawData)
|
|
@@ -2166,15 +2134,24 @@ export default class NestAccfactory {
|
|
|
2166
2134
|
.forEach(([object_key, value]) => {
|
|
2167
2135
|
let tempDevice = {};
|
|
2168
2136
|
try {
|
|
2169
|
-
if (value
|
|
2137
|
+
if (value?.source === NestAccfactory.DataSource.PROTOBUF && value.value?.streaming_protocol !== undefined) {
|
|
2170
2138
|
let RESTTypeData = {};
|
|
2171
|
-
//
|
|
2172
|
-
|
|
2173
|
-
|
|
2139
|
+
// If we haven't found a macaddress, ase a Nest Labs prefix for first 6 digits followed by a CRC24 based off serial number for last 6 digits.
|
|
2140
|
+
RESTTypeData.mac_address =
|
|
2141
|
+
value.value?.wifi_interface?.macAddress !== undefined
|
|
2142
|
+
? Buffer.from(value.value.wifi_interface.macAddress, 'base64')
|
|
2143
|
+
: '18B430' + crc24(value.value.device_identity.serialNumber.toUpperCase());
|
|
2174
2144
|
RESTTypeData.serial_number = value.value.device_identity.serialNumber;
|
|
2175
|
-
RESTTypeData.software_version =
|
|
2145
|
+
RESTTypeData.software_version =
|
|
2146
|
+
value.value?.floodlight_settings?.associatedFloodlightFirmwareVersion !== undefined
|
|
2147
|
+
? value.value.floodlight_settings.associatedFloodlightFirmwareVersion
|
|
2148
|
+
: value.value.device_identity.softwareVersion.replace(/[^0-9.]/g, '');
|
|
2176
2149
|
RESTTypeData.model = 'Camera';
|
|
2177
|
-
if (
|
|
2150
|
+
if (
|
|
2151
|
+
value.value.device_info.typeName === 'google.resource.NeonQuartzResource' &&
|
|
2152
|
+
value.value?.floodlight_settings === undefined &&
|
|
2153
|
+
value.value?.floodlight_state === undefined
|
|
2154
|
+
) {
|
|
2178
2155
|
RESTTypeData.model = 'Cam (battery)';
|
|
2179
2156
|
}
|
|
2180
2157
|
if (value.value.device_info.typeName === 'google.resource.GreenQuartzResource') {
|
|
@@ -2198,17 +2175,19 @@ export default class NestAccfactory {
|
|
|
2198
2175
|
if (value.value.device_info.typeName === 'nest.resource.NestHelloResource') {
|
|
2199
2176
|
RESTTypeData.model = 'Doorbell (wired, 1st gen)';
|
|
2200
2177
|
}
|
|
2201
|
-
if (
|
|
2178
|
+
if (
|
|
2179
|
+
value.value.device_info.typeName === 'google.resource.NeonQuartzResource' &&
|
|
2180
|
+
value.value?.floodlight_settings !== undefined &&
|
|
2181
|
+
value.value?.floodlight_state !== undefined
|
|
2182
|
+
) {
|
|
2202
2183
|
RESTTypeData.model = 'Cam with Floodlight (wired)';
|
|
2203
2184
|
}
|
|
2204
|
-
|
|
2205
|
-
RESTTypeData.model = 'Hub Max';
|
|
2206
|
-
}
|
|
2185
|
+
|
|
2207
2186
|
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
2208
|
-
RESTTypeData.description =
|
|
2187
|
+
RESTTypeData.description = value.value?.label?.label !== undefined ? value.value.label.label : '';
|
|
2209
2188
|
RESTTypeData.location = get_location_name(
|
|
2210
|
-
value.value
|
|
2211
|
-
value.value
|
|
2189
|
+
value.value?.device_info?.pairerId?.resourceId,
|
|
2190
|
+
value.value?.device_located_settings?.whereAnnotationRid?.resourceId,
|
|
2212
2191
|
);
|
|
2213
2192
|
RESTTypeData.audio_enabled = value.value?.microphone_settings?.enableMicrophone === true;
|
|
2214
2193
|
RESTTypeData.has_indoor_chime =
|
|
@@ -2228,7 +2207,7 @@ export default class NestAccfactory {
|
|
|
2228
2207
|
value.value.activity_zone_settings.activityZones.forEach((zone) => {
|
|
2229
2208
|
RESTTypeData.activity_zones.push({
|
|
2230
2209
|
id: typeof zone.zoneProperties?.zoneId === 'number' ? zone.zoneProperties.zoneId : zone.zoneProperties.internalIndex,
|
|
2231
|
-
name: makeHomeKitName(
|
|
2210
|
+
name: makeHomeKitName(zone.zoneProperties?.name !== undefined ? zone.zoneProperties.name : ''),
|
|
2232
2211
|
hidden: false,
|
|
2233
2212
|
uri: '',
|
|
2234
2213
|
});
|
|
@@ -2244,10 +2223,18 @@ export default class NestAccfactory {
|
|
|
2244
2223
|
RESTTypeData.streaming_host =
|
|
2245
2224
|
typeof value.value?.streaming_protocol?.directHost?.value === 'string' ? value.value.streaming_protocol.directHost.value : '';
|
|
2246
2225
|
|
|
2226
|
+
// Floodlight settings/status
|
|
2227
|
+
RESTTypeData.has_light = value.value?.floodlight_settings !== undefined && value.value?.floodlight_state !== undefined;
|
|
2228
|
+
RESTTypeData.light_enabled = value.value?.floodlight_state?.currentState === 'LIGHT_STATE_ON';
|
|
2229
|
+
RESTTypeData.light_brightness =
|
|
2230
|
+
value.value?.floodlight_settings?.brightness !== undefined
|
|
2231
|
+
? scaleValue(value.value.floodlight_settings.brightness, 0, 10, 0, 100)
|
|
2232
|
+
: 0;
|
|
2233
|
+
|
|
2247
2234
|
tempDevice = process_camera_doorbell_data(object_key, RESTTypeData);
|
|
2248
2235
|
}
|
|
2249
2236
|
|
|
2250
|
-
if (value
|
|
2237
|
+
if (value?.source === NestAccfactory.DataSource.REST && value.value?.properties?.['cc2migration.overview_state'] === 'NORMAL') {
|
|
2251
2238
|
// We'll only use the REST API data for Camera's which have NOT been migrated to Google Home
|
|
2252
2239
|
let RESTTypeData = {};
|
|
2253
2240
|
RESTTypeData.mac_address = value.value.mac_address;
|
|
@@ -2260,17 +2247,17 @@ export default class NestAccfactory {
|
|
|
2260
2247
|
RESTTypeData.nexus_api_http_server_url = value.value.nexus_api_http_server_url;
|
|
2261
2248
|
RESTTypeData.online = value.value.streaming_state.includes('offline') === false;
|
|
2262
2249
|
RESTTypeData.audio_enabled = value.value.audio_input_enabled === true;
|
|
2263
|
-
RESTTypeData.has_indoor_chime = value.value
|
|
2264
|
-
RESTTypeData.indoor_chime_enabled = value.value
|
|
2265
|
-
RESTTypeData.has_irled = value.value
|
|
2266
|
-
RESTTypeData.irled_enabled = value.value
|
|
2267
|
-
RESTTypeData.has_statusled = value.value
|
|
2268
|
-
RESTTypeData.has_video_flip = value.value
|
|
2269
|
-
RESTTypeData.video_flipped = value.value
|
|
2270
|
-
RESTTypeData.statusled_brightness = value.value
|
|
2271
|
-
RESTTypeData.has_microphone = value.value
|
|
2272
|
-
RESTTypeData.has_speaker = value.value
|
|
2273
|
-
RESTTypeData.has_motion_detection = value.value
|
|
2250
|
+
RESTTypeData.has_indoor_chime = value.value?.capabilities.includes('indoor_chime') === true;
|
|
2251
|
+
RESTTypeData.indoor_chime_enabled = value.value?.properties['doorbell.indoor_chime.enabled'] === true;
|
|
2252
|
+
RESTTypeData.has_irled = value.value?.capabilities.includes('irled') === true;
|
|
2253
|
+
RESTTypeData.irled_enabled = value.value?.properties['irled.state'] !== 'always_off';
|
|
2254
|
+
RESTTypeData.has_statusled = value.value?.capabilities.includes('statusled') === true;
|
|
2255
|
+
RESTTypeData.has_video_flip = value.value?.capabilities.includes('video.flip') === true;
|
|
2256
|
+
RESTTypeData.video_flipped = value.value?.properties['video.flipped'] === true;
|
|
2257
|
+
RESTTypeData.statusled_brightness = value.value?.properties['statusled.brightness'];
|
|
2258
|
+
RESTTypeData.has_microphone = value.value?.capabilities.includes('audio.microphone') === true;
|
|
2259
|
+
RESTTypeData.has_speaker = value.value?.capabilities.includes('audio.speaker') === true;
|
|
2260
|
+
RESTTypeData.has_motion_detection = value.value?.capabilities.includes('detectors.on_camera') === true;
|
|
2274
2261
|
RESTTypeData.activity_zones = value.value.activity_zones; // structure elements we added
|
|
2275
2262
|
RESTTypeData.alerts = typeof value.value?.alerts === 'object' ? value.value.alerts : [];
|
|
2276
2263
|
RESTTypeData.streaming_protocols = ['PROTOCOL_NEXUSTALK'];
|
|
@@ -2316,7 +2303,7 @@ export default class NestAccfactory {
|
|
|
2316
2303
|
}
|
|
2317
2304
|
});
|
|
2318
2305
|
|
|
2319
|
-
// Process data for any structure(s) for both REST and
|
|
2306
|
+
// Process data for any structure(s) for both REST and Protobuf API data
|
|
2320
2307
|
// We use this to created virtual weather station(s) for each structure that has location data
|
|
2321
2308
|
const process_structure_data = (object_key, data) => {
|
|
2322
2309
|
let processed = {};
|
|
@@ -2392,13 +2379,12 @@ export default class NestAccfactory {
|
|
|
2392
2379
|
.forEach(([object_key, value]) => {
|
|
2393
2380
|
let tempDevice = {};
|
|
2394
2381
|
try {
|
|
2395
|
-
if (value
|
|
2382
|
+
if (value?.source === NestAccfactory.DataSource.PROTOBUF) {
|
|
2396
2383
|
let RESTTypeData = {};
|
|
2397
2384
|
RESTTypeData.postal_code = value.value.structure_location.postalCode.value;
|
|
2398
2385
|
RESTTypeData.country_code = value.value.structure_location.countryCode.value;
|
|
2399
|
-
RESTTypeData.city =
|
|
2400
|
-
RESTTypeData.state =
|
|
2401
|
-
typeof value.value.structure_location?.state === 'string' ? value.value.structure_location.state.value : '';
|
|
2386
|
+
RESTTypeData.city = value.value?.structure_location?.city !== undefined ? value.value.structure_location.city.value : '';
|
|
2387
|
+
RESTTypeData.state = value.value?.structure_location?.state !== undefined ? value.value.structure_location.state.value : '';
|
|
2402
2388
|
RESTTypeData.latitude = value.value.structure_location.geoCoordinate.latitude;
|
|
2403
2389
|
RESTTypeData.longitude = value.value.structure_location.geoCoordinate.longitude;
|
|
2404
2390
|
RESTTypeData.description =
|
|
@@ -2407,16 +2393,16 @@ export default class NestAccfactory {
|
|
|
2407
2393
|
: value.value.structure_info.name;
|
|
2408
2394
|
RESTTypeData.weather = value.value.weather;
|
|
2409
2395
|
|
|
2410
|
-
// Use the REST API structure ID from the
|
|
2396
|
+
// Use the REST API structure ID from the Protobuf structure. This should prevent two 'weather' objects being created
|
|
2411
2397
|
let tempDevice = process_structure_data(value.value.structure_info.rtsStructureId, RESTTypeData);
|
|
2412
|
-
tempDevice.uuid = object_key; // Use the
|
|
2398
|
+
tempDevice.uuid = object_key; // Use the Protobuf structure ID post processing
|
|
2413
2399
|
}
|
|
2414
|
-
if (value
|
|
2400
|
+
if (value?.source === NestAccfactory.DataSource.REST) {
|
|
2415
2401
|
let RESTTypeData = {};
|
|
2416
2402
|
RESTTypeData.postal_code = value.value.postal_code;
|
|
2417
2403
|
RESTTypeData.country_code = value.value.country_code;
|
|
2418
|
-
RESTTypeData.city =
|
|
2419
|
-
RESTTypeData.state =
|
|
2404
|
+
RESTTypeData.city = value.value?.city !== undefined ? value.value.city : '';
|
|
2405
|
+
RESTTypeData.state = value.value?.state !== undefined ? value.value.state : '';
|
|
2420
2406
|
RESTTypeData.latitude = value.value.latitude;
|
|
2421
2407
|
RESTTypeData.longitude = value.value.longitude;
|
|
2422
2408
|
RESTTypeData.description =
|
|
@@ -2443,25 +2429,25 @@ export default class NestAccfactory {
|
|
|
2443
2429
|
async #set(deviceUUID, values) {
|
|
2444
2430
|
if (
|
|
2445
2431
|
typeof deviceUUID !== 'string' ||
|
|
2446
|
-
typeof this.#rawData[deviceUUID] !== 'object' ||
|
|
2432
|
+
typeof this.#rawData?.[deviceUUID] !== 'object' ||
|
|
2447
2433
|
typeof values !== 'object' ||
|
|
2448
|
-
typeof this.#connections[this.#rawData[deviceUUID]?.connection] !== 'object'
|
|
2434
|
+
typeof this.#connections[this.#rawData?.[deviceUUID]?.connection] !== 'object'
|
|
2449
2435
|
) {
|
|
2450
2436
|
return;
|
|
2451
2437
|
}
|
|
2452
2438
|
|
|
2453
2439
|
if (
|
|
2454
2440
|
this.#connections[this.#rawData[deviceUUID].connection].protobufRoot !== null &&
|
|
2455
|
-
this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.PROTOBUF
|
|
2441
|
+
this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.PROTOBUF
|
|
2456
2442
|
) {
|
|
2457
|
-
let
|
|
2458
|
-
let setDataToEncode = [];
|
|
2443
|
+
let updatedTraits = [];
|
|
2459
2444
|
let protobufElement = {
|
|
2460
|
-
|
|
2445
|
+
traitRequest: {
|
|
2461
2446
|
resourceId: deviceUUID,
|
|
2462
2447
|
traitLabel: '',
|
|
2448
|
+
requestId: crypto.randomUUID(),
|
|
2463
2449
|
},
|
|
2464
|
-
|
|
2450
|
+
state: {
|
|
2465
2451
|
type_url: '',
|
|
2466
2452
|
value: {},
|
|
2467
2453
|
},
|
|
@@ -2470,9 +2456,9 @@ export default class NestAccfactory {
|
|
|
2470
2456
|
await Promise.all(
|
|
2471
2457
|
Object.entries(values).map(async ([key, value]) => {
|
|
2472
2458
|
// Reset elements at start of loop
|
|
2473
|
-
protobufElement.
|
|
2474
|
-
protobufElement.
|
|
2475
|
-
protobufElement.
|
|
2459
|
+
protobufElement.traitRequest.traitLabel = '';
|
|
2460
|
+
protobufElement.state.type_url = '';
|
|
2461
|
+
protobufElement.state.value = {};
|
|
2476
2462
|
|
|
2477
2463
|
if (
|
|
2478
2464
|
(key === 'hvac_mode' &&
|
|
@@ -2482,45 +2468,48 @@ export default class NestAccfactory {
|
|
|
2482
2468
|
value.toUpperCase() === 'HEAT' ||
|
|
2483
2469
|
value.toUpperCase() === 'RANGE')) ||
|
|
2484
2470
|
(key === 'target_temperature' &&
|
|
2485
|
-
this.#rawData[deviceUUID]
|
|
2471
|
+
this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode === 'ECO_MODE_INACTIVE' &&
|
|
2486
2472
|
typeof value === 'number') ||
|
|
2487
2473
|
(key === 'target_temperature_low' &&
|
|
2488
|
-
this.#rawData[deviceUUID]
|
|
2474
|
+
this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode === 'ECO_MODE_INACTIVE' &&
|
|
2489
2475
|
typeof value === 'number') ||
|
|
2490
2476
|
(key === 'target_temperature_high' &&
|
|
2491
|
-
this.#rawData[deviceUUID]
|
|
2477
|
+
this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode === 'ECO_MODE_INACTIVE' &&
|
|
2492
2478
|
typeof value === 'number')
|
|
2493
2479
|
) {
|
|
2494
2480
|
// Set either the 'mode' and/or non-eco temperatures on the target thermostat
|
|
2495
|
-
|
|
2496
|
-
|
|
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;
|
|
2497
2484
|
|
|
2498
2485
|
if (
|
|
2499
|
-
key === 'target_temperature_low' ||
|
|
2500
|
-
(
|
|
2501
|
-
|
|
2486
|
+
(key === 'target_temperature_low' || key === 'target_temperature') &&
|
|
2487
|
+
(protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_HEAT' ||
|
|
2488
|
+
protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_RANGE')
|
|
2502
2489
|
) {
|
|
2503
|
-
|
|
2490
|
+
// Changing heating target temperature
|
|
2491
|
+
protobufElement.state.value.targetTemperature.heatingTarget = { value: value };
|
|
2504
2492
|
}
|
|
2505
2493
|
if (
|
|
2506
|
-
key === 'target_temperature_high' ||
|
|
2507
|
-
(
|
|
2508
|
-
|
|
2494
|
+
(key === 'target_temperature_high' || key === 'target_temperature') &&
|
|
2495
|
+
(protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_COOL' ||
|
|
2496
|
+
protobufElement.state.value.targetTemperature.setpointType === 'SET_POINT_TYPE_RANGE')
|
|
2509
2497
|
) {
|
|
2510
|
-
|
|
2498
|
+
// Changing cooling target temperature
|
|
2499
|
+
protobufElement.state.value.targetTemperature.coolingTarget = { value: value };
|
|
2511
2500
|
}
|
|
2512
2501
|
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
protobufElement.
|
|
2502
|
+
if (key === 'hvac_mode' && value.toUpperCase() !== 'OFF') {
|
|
2503
|
+
protobufElement.state.value.targetTemperature.setpointType = 'SET_POINT_TYPE_' + value.toUpperCase();
|
|
2504
|
+
protobufElement.state.value.enabled = { value: true };
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
if (key === 'hvac_mode' && value.toUpperCase() === 'OFF') {
|
|
2508
|
+
protobufElement.state.value.enabled = { value: false };
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// Tage 'who is doing the temperature/mode change. We are :-)
|
|
2512
|
+
protobufElement.state.value.targetTemperature.currentActorInfo = {
|
|
2524
2513
|
method: 'HVAC_ACTOR_METHOD_IOS',
|
|
2525
2514
|
originator: {
|
|
2526
2515
|
resourceId: Object.keys(this.#rawData)
|
|
@@ -2530,76 +2519,62 @@ export default class NestAccfactory {
|
|
|
2530
2519
|
timeOfAction: { seconds: Math.floor(Date.now() / 1000), nanos: (Date.now() % 1000) * 1e6 },
|
|
2531
2520
|
originatorRtsId: '',
|
|
2532
2521
|
};
|
|
2533
|
-
protobufElement.property.value.targetTemperature.originalActorInfo = {
|
|
2534
|
-
method: 'HVAC_ACTOR_METHOD_UNSPECIFIED',
|
|
2535
|
-
originator: null,
|
|
2536
|
-
timeOfAction: null,
|
|
2537
|
-
originatorRtsId: '',
|
|
2538
|
-
};
|
|
2539
|
-
protobufElement.property.value.enabled = {
|
|
2540
|
-
value:
|
|
2541
|
-
key === 'hvac_mode'
|
|
2542
|
-
? value.toUpperCase() !== 'OFF'
|
|
2543
|
-
: this.#rawData[deviceUUID].value.target_temperature_settings.enabled.value,
|
|
2544
|
-
};
|
|
2545
2522
|
}
|
|
2546
2523
|
|
|
2547
2524
|
if (
|
|
2548
2525
|
(key === 'target_temperature' &&
|
|
2549
|
-
this.#rawData[deviceUUID]
|
|
2526
|
+
this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE' &&
|
|
2550
2527
|
typeof value === 'number') ||
|
|
2551
2528
|
(key === 'target_temperature_low' &&
|
|
2552
|
-
this.#rawData[deviceUUID]
|
|
2529
|
+
this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE' &&
|
|
2553
2530
|
typeof value === 'number') ||
|
|
2554
2531
|
(key === 'target_temperature_high' &&
|
|
2555
|
-
this.#rawData[deviceUUID]
|
|
2532
|
+
this.#rawData?.[deviceUUID]?.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE' &&
|
|
2556
2533
|
typeof value === 'number')
|
|
2557
2534
|
) {
|
|
2558
2535
|
// Set eco mode temperatures on the target thermostat
|
|
2559
|
-
protobufElement.
|
|
2560
|
-
protobufElement.
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
protobufElement.
|
|
2564
|
-
protobufElement.
|
|
2565
|
-
protobufElement.
|
|
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
|
|
2566
2543
|
? value
|
|
2567
|
-
: protobufElement.
|
|
2568
|
-
protobufElement.
|
|
2569
|
-
protobufElement.
|
|
2570
|
-
protobufElement.
|
|
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
|
|
2571
2548
|
? value
|
|
2572
|
-
: protobufElement.
|
|
2573
|
-
protobufElement.
|
|
2574
|
-
protobufElement.
|
|
2575
|
-
protobufElement.
|
|
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 &&
|
|
2576
2553
|
key === 'target_temperature_low'
|
|
2577
2554
|
? value
|
|
2578
|
-
: protobufElement.
|
|
2579
|
-
protobufElement.
|
|
2580
|
-
protobufElement.
|
|
2581
|
-
protobufElement.
|
|
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 &&
|
|
2582
2559
|
key === 'target_temperature_high'
|
|
2583
2560
|
? value
|
|
2584
|
-
: protobufElement.
|
|
2561
|
+
: protobufElement.state.value.ecoTemperatureCool.value.value;
|
|
2585
2562
|
}
|
|
2586
2563
|
|
|
2587
2564
|
if (key === 'temperature_scale' && typeof value === 'string' && (value.toUpperCase() === 'C' || value.toUpperCase() === 'F')) {
|
|
2588
2565
|
// Set the temperature scale on the target thermostat
|
|
2589
|
-
protobufElement.
|
|
2590
|
-
protobufElement.
|
|
2591
|
-
|
|
2592
|
-
protobufElement.
|
|
2593
|
-
protobufElement.property.value.temperatureScale = value.toUpperCase() === 'F' ? 'TEMPERATURE_SCALE_F' : 'TEMPERATURE_SCALE_C';
|
|
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';
|
|
2594
2570
|
}
|
|
2595
2571
|
|
|
2596
2572
|
if (key === 'temperature_lock' && typeof value === 'boolean') {
|
|
2597
2573
|
// Set lock mode on the target thermostat
|
|
2598
|
-
protobufElement.
|
|
2599
|
-
protobufElement.
|
|
2600
|
-
|
|
2601
|
-
protobufElement.
|
|
2602
|
-
protobufElement.property.value.enabled = value === true;
|
|
2574
|
+
protobufElement.traitRequest.traitLabel = 'temperature_lock_settings';
|
|
2575
|
+
protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.TemperatureLockSettingsTrait';
|
|
2576
|
+
protobufElement.state.value = this.#rawData[deviceUUID].value.temperature_lock_settings;
|
|
2577
|
+
protobufElement.state.value.enabled = value === true;
|
|
2603
2578
|
}
|
|
2604
2579
|
|
|
2605
2580
|
if (key === 'fan_state' && typeof value === 'boolean') {
|
|
@@ -2609,11 +2584,10 @@ export default class NestAccfactory {
|
|
|
2609
2584
|
? Math.floor(Date.now() / 1000) + this.#rawData[deviceUUID].value.fan_control_settings.timerDuration.seconds
|
|
2610
2585
|
: 0;
|
|
2611
2586
|
|
|
2612
|
-
protobufElement.
|
|
2613
|
-
protobufElement.
|
|
2614
|
-
|
|
2615
|
-
protobufElement.
|
|
2616
|
-
protobufElement.property.value.timerEnd = { seconds: endTime, nanos: (endTime % 1000) * 1e6 };
|
|
2587
|
+
protobufElement.traitRequest.traitLabel = 'fan_control_settings';
|
|
2588
|
+
protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.FanControlSettingsTrait';
|
|
2589
|
+
protobufElement.state.value = this.#rawData[deviceUUID].value.fan_control_settings;
|
|
2590
|
+
protobufElement.state.value.timerEnd = { seconds: endTime, nanos: (endTime % 1000) * 1e6 };
|
|
2617
2591
|
}
|
|
2618
2592
|
|
|
2619
2593
|
//if (key === 'statusled.brightness'
|
|
@@ -2621,128 +2595,155 @@ export default class NestAccfactory {
|
|
|
2621
2595
|
|
|
2622
2596
|
if (key === 'streaming.enabled' && typeof value === 'boolean') {
|
|
2623
2597
|
// Turn camera video on/off
|
|
2624
|
-
protobufElement.
|
|
2625
|
-
protobufElement.
|
|
2626
|
-
|
|
2627
|
-
protobufElement.
|
|
2628
|
-
protobufElement.
|
|
2629
|
-
protobufElement.
|
|
2630
|
-
protobufElement.property.value.settingsUpdated = { seconds: Math.floor(Date.now() / 1000), nanos: (Date.now() % 1000) * 1e6 };
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
if (key === 'watermark.enabled' && typeof value === 'boolean') {
|
|
2634
|
-
// Unsupported via protobuf?
|
|
2598
|
+
protobufElement.traitRequest.traitLabel = 'recording_toggle_settings';
|
|
2599
|
+
protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.product.camera.RecordingToggleSettingsTrait';
|
|
2600
|
+
protobufElement.state.value = this.#rawData[deviceUUID].value.recording_toggle_settings;
|
|
2601
|
+
protobufElement.state.value.targetCameraState = value === true ? 'CAMERA_ON' : 'CAMERA_OFF';
|
|
2602
|
+
protobufElement.state.value.changeModeReason = 2;
|
|
2603
|
+
protobufElement.state.value.settingsUpdated = { seconds: Math.floor(Date.now() / 1000), nanos: (Date.now() % 1000) * 1e6 };
|
|
2635
2604
|
}
|
|
2636
2605
|
|
|
2637
2606
|
if (key === 'audio.enabled' && typeof value === 'boolean') {
|
|
2638
2607
|
// Enable/disable microphone on camera/doorbell
|
|
2639
|
-
protobufElement.
|
|
2640
|
-
protobufElement.
|
|
2641
|
-
|
|
2642
|
-
protobufElement.
|
|
2643
|
-
protobufElement.property.value.enableMicrophone = value;
|
|
2608
|
+
protobufElement.traitRequest.traitLabel = 'microphone_settings';
|
|
2609
|
+
protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.audio.MicrophoneSettingsTrait';
|
|
2610
|
+
protobufElement.state.value = this.#rawData[deviceUUID].value.microphone_settings;
|
|
2611
|
+
protobufElement.state.value.enableMicrophone = value;
|
|
2644
2612
|
}
|
|
2645
2613
|
|
|
2646
|
-
if (key === '
|
|
2614
|
+
if (key === 'indoor_chime_enabled' && typeof value === 'boolean') {
|
|
2647
2615
|
// Enable/disable chime status on doorbell
|
|
2648
|
-
protobufElement.
|
|
2649
|
-
protobufElement.
|
|
2650
|
-
|
|
2651
|
-
protobufElement.
|
|
2652
|
-
|
|
2616
|
+
protobufElement.traitRequest.traitLabel = 'doorbell_indoor_chime_settings';
|
|
2617
|
+
protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.product.doorbell.DoorbellIndoorChimeSettingsTrait';
|
|
2618
|
+
protobufElement.state.value = this.#rawData[deviceUUID].value.doorbell_indoor_chime_settings;
|
|
2619
|
+
protobufElement.state.value.chimeEnabled = value;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
if (key === 'light_enabled' && typeof value === 'boolean') {
|
|
2623
|
+
// Turn on/off light on supported camera devices. Need to find the related or SERVICE__ object for teh device
|
|
2624
|
+
let serviceUUID = undefined;
|
|
2625
|
+
if (this.#rawData[deviceUUID].value?.related_resources?.relatedResources !== undefined) {
|
|
2626
|
+
Object.values(this.#rawData[deviceUUID].value?.related_resources?.relatedResources).forEach((values) => {
|
|
2627
|
+
if (
|
|
2628
|
+
values?.resourceTypeName?.resourceName === 'google.resource.AzizResource' &&
|
|
2629
|
+
values?.resourceId?.resourceId.startsWith('SERVICE_') === true
|
|
2630
|
+
) {
|
|
2631
|
+
serviceUUID = values.resourceId.resourceId;
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2635
|
+
if (serviceUUID !== undefined) {
|
|
2636
|
+
let commandResponse = await this.#protobufCommand(this.#rawData[deviceUUID].connection, 'ResourceApi', 'SendCommand', {
|
|
2637
|
+
resourceRequest: {
|
|
2638
|
+
resourceId: serviceUUID,
|
|
2639
|
+
requestId: crypto.randomUUID(),
|
|
2640
|
+
},
|
|
2641
|
+
resourceCommands: [
|
|
2642
|
+
{
|
|
2643
|
+
traitLabel: 'on_off',
|
|
2644
|
+
command: {
|
|
2645
|
+
type_url: 'type.nestlabs.com/weave.trait.actuator.OnOffTrait.SetStateRequest',
|
|
2646
|
+
value: {
|
|
2647
|
+
on: value,
|
|
2648
|
+
},
|
|
2649
|
+
},
|
|
2650
|
+
},
|
|
2651
|
+
],
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2654
|
+
if (commandResponse.sendCommandResponse?.[0]?.traitOperations?.[0]?.progress !== 'COMPLETE') {
|
|
2655
|
+
this?.log?.debug && this.log.debug('Protobuf API had error setting light status on uuid "%s"', deviceUUID);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2653
2659
|
}
|
|
2654
2660
|
|
|
2655
|
-
if (
|
|
2656
|
-
|
|
2661
|
+
if (key === 'light_brightness' && typeof value === 'number') {
|
|
2662
|
+
// Set light brightness on supported camera devices
|
|
2663
|
+
protobufElement.traitRequest.traitLabel = 'floodlight_settings';
|
|
2664
|
+
protobufElement.state.type_url = 'type.nestlabs.com/google.trait.product.camera.FloodlightSettingsTrait';
|
|
2665
|
+
protobufElement.state.value = this.#rawData[deviceUUID].value.floodlight_settings;
|
|
2666
|
+
protobufElement.state.value.brightness = scaleValue(value, 0, 100, 0, 10); // Scale to required level
|
|
2657
2667
|
}
|
|
2658
2668
|
|
|
2659
|
-
if (protobufElement.
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2669
|
+
if (protobufElement.traitRequest.traitLabel === '' || protobufElement.state.type_url === '') {
|
|
2670
|
+
this?.log?.debug && this.log.debug('Unknown Protobuf set key "%s" for device uuid "%s"', key, deviceUUID);
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
if (protobufElement.traitRequest.traitLabel !== '' && protobufElement.state.type_url !== '') {
|
|
2664
2674
|
// eslint-disable-next-line no-undef
|
|
2665
|
-
|
|
2675
|
+
updatedTraits.push(structuredClone(protobufElement));
|
|
2666
2676
|
}
|
|
2667
2677
|
}),
|
|
2668
2678
|
);
|
|
2669
2679
|
|
|
2670
|
-
if (
|
|
2671
|
-
let
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
Authorization: 'Basic ' + this.#connections[this.#rawData[deviceUUID].connection].token,
|
|
2681
|
-
'Content-Type': 'application/x-protobuf',
|
|
2682
|
-
'X-Accept-Content-Transfer-Encoding': 'binary',
|
|
2683
|
-
'X-Accept-Response-Streaming': 'true',
|
|
2684
|
-
},
|
|
2685
|
-
data: encodedData,
|
|
2686
|
-
};
|
|
2687
|
-
axios(request)
|
|
2688
|
-
.then((response) => {
|
|
2689
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
2690
|
-
throw new Error('protobuf API had error updating device traits');
|
|
2691
|
-
}
|
|
2692
|
-
})
|
|
2693
|
-
.catch((error) => {
|
|
2694
|
-
this?.log?.debug &&
|
|
2695
|
-
this.log.debug('protobuf API had error updating device traits for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2696
|
-
});
|
|
2680
|
+
if (updatedTraits.length !== 0) {
|
|
2681
|
+
let commandResponse = await this.#protobufCommand(this.#rawData[deviceUUID].connection, 'TraitBatchApi', 'BatchUpdateState', {
|
|
2682
|
+
batchUpdateStateRequest: updatedTraits,
|
|
2683
|
+
});
|
|
2684
|
+
if (
|
|
2685
|
+
commandResponse === undefined ||
|
|
2686
|
+
commandResponse?.batchUpdateStateResponse?.[0]?.traitOperations?.[0]?.progress !== 'COMPLETE'
|
|
2687
|
+
) {
|
|
2688
|
+
this?.log?.debug && this.log.debug('Protobuf API had error updating device traits for uuid "%s"', deviceUUID);
|
|
2689
|
+
}
|
|
2697
2690
|
}
|
|
2698
2691
|
}
|
|
2699
2692
|
|
|
2700
|
-
if (this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.REST && deviceUUID.startsWith('quartz.') === true) {
|
|
2693
|
+
if (this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.REST && deviceUUID.startsWith('quartz.') === true) {
|
|
2701
2694
|
// Set value on Nest Camera/Doorbell
|
|
2702
2695
|
await Promise.all(
|
|
2703
2696
|
Object.entries(values).map(async ([key, value]) => {
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
'content-type': 'application/x-www-form-urlencoded',
|
|
2711
|
-
[this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.key]:
|
|
2712
|
-
this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.value +
|
|
2713
|
-
this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.token,
|
|
2714
|
-
},
|
|
2715
|
-
responseType: 'json',
|
|
2716
|
-
timeout: NESTAPITIMEOUT,
|
|
2717
|
-
data: [key] + '=' + value + '&uuid=' + deviceUUID.split('.')[1],
|
|
2697
|
+
const SETPROPERTIES = {
|
|
2698
|
+
indoor_chime_enabled: 'doorbell.indoor_chime.enabled',
|
|
2699
|
+
statusled_brightness: 'statusled.brightness',
|
|
2700
|
+
irled_enabled: 'irled.state',
|
|
2701
|
+
streaming_enabled: 'streaming.enabled',
|
|
2702
|
+
audio_enabled: 'audio.enabled',
|
|
2718
2703
|
};
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2704
|
+
|
|
2705
|
+
// Transform key to correct set camera properties key
|
|
2706
|
+
key = SETPROPERTIES[key] !== undefined ? SETPROPERTIES[key] : key;
|
|
2707
|
+
|
|
2708
|
+
await fetchWrapper(
|
|
2709
|
+
'post',
|
|
2710
|
+
'https://webapi.' + this.#connections[this.#rawData[deviceUUID].connection].cameraAPIHost + '/api/dropcams.set_properties',
|
|
2711
|
+
{
|
|
2712
|
+
headers: {
|
|
2713
|
+
referer: 'https://' + this.#connections[this.#rawData[deviceUUID].connection].referer,
|
|
2714
|
+
'User-Agent': USERAGENT,
|
|
2715
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2716
|
+
[this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.key]:
|
|
2717
|
+
this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.value +
|
|
2718
|
+
this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.token,
|
|
2719
|
+
},
|
|
2720
|
+
timeout: NESTAPITIMEOUT,
|
|
2721
|
+
},
|
|
2722
|
+
[key] + '=' + value + '&uuid=' + deviceUUID.split('.')[1],
|
|
2723
|
+
)
|
|
2724
|
+
.then((response) => response.json())
|
|
2725
|
+
.then((data) => {
|
|
2726
|
+
if (data?.status !== 0) {
|
|
2727
2727
|
throw new Error('REST API camera update for failed with error');
|
|
2728
2728
|
}
|
|
2729
2729
|
})
|
|
2730
2730
|
.catch((error) => {
|
|
2731
|
-
this?.log?.debug
|
|
2731
|
+
if (error?.name !== 'TimeoutError' && this?.log?.debug) {
|
|
2732
2732
|
this.log.debug('REST API camera update for failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2733
|
+
}
|
|
2733
2734
|
});
|
|
2734
2735
|
}),
|
|
2735
2736
|
);
|
|
2736
2737
|
}
|
|
2737
2738
|
|
|
2738
|
-
if (this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.REST && deviceUUID.startsWith('quartz.') === false) {
|
|
2739
|
+
if (this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.REST && deviceUUID.startsWith('quartz.') === false) {
|
|
2739
2740
|
// set values on other Nest devices besides cameras/doorbells
|
|
2740
2741
|
await Promise.all(
|
|
2741
2742
|
Object.entries(values).map(async ([key, value]) => {
|
|
2742
|
-
let
|
|
2743
|
+
let subscribeJSONData = { objects: [] };
|
|
2743
2744
|
|
|
2744
2745
|
if (deviceUUID.startsWith('device.') === false) {
|
|
2745
|
-
|
|
2746
|
+
subscribeJSONData.objects.push({ object_key: deviceUUID, op: 'MERGE', value: { [key]: value } });
|
|
2746
2747
|
}
|
|
2747
2748
|
|
|
2748
2749
|
// Some elements when setting thermostat data are located in a different object locations than with the device object
|
|
@@ -2763,30 +2764,25 @@ export default class NestAccfactory {
|
|
|
2763
2764
|
) {
|
|
2764
2765
|
RESTStructureUUID = 'shared.' + deviceUUID.split('.')[1];
|
|
2765
2766
|
}
|
|
2766
|
-
|
|
2767
|
+
subscribeJSONData.objects.push({ object_key: RESTStructureUUID, op: 'MERGE', value: { [key]: value } });
|
|
2767
2768
|
}
|
|
2768
2769
|
|
|
2769
|
-
if (
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2770
|
+
if (subscribeJSONData.objects.length !== 0) {
|
|
2771
|
+
await fetchWrapper(
|
|
2772
|
+
'post',
|
|
2773
|
+
this.#connections[this.#rawData[deviceUUID].connection].transport_url + '/v5/put',
|
|
2774
|
+
{
|
|
2775
|
+
referer: 'https://' + this.#connections[this.#rawData[deviceUUID].connection].referer,
|
|
2776
|
+
headers: {
|
|
2777
|
+
'User-Agent': USERAGENT,
|
|
2778
|
+
Authorization: 'Basic ' + this.#connections[this.#rawData[deviceUUID].connection].token,
|
|
2779
|
+
},
|
|
2777
2780
|
},
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
throw new Error('REST API property update for failed with error');
|
|
2784
|
-
}
|
|
2785
|
-
})
|
|
2786
|
-
.catch((error) => {
|
|
2787
|
-
this?.log?.debug &&
|
|
2788
|
-
this.log.debug('REST API property update for failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2789
|
-
});
|
|
2781
|
+
JSON.stringify(subscribeJSONData),
|
|
2782
|
+
).catch((error) => {
|
|
2783
|
+
this?.log?.debug &&
|
|
2784
|
+
this.log.debug('REST API property update for failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2785
|
+
});
|
|
2790
2786
|
}
|
|
2791
2787
|
}),
|
|
2792
2788
|
);
|
|
@@ -2796,9 +2792,9 @@ export default class NestAccfactory {
|
|
|
2796
2792
|
async #get(deviceUUID, values) {
|
|
2797
2793
|
if (
|
|
2798
2794
|
typeof deviceUUID !== 'string' ||
|
|
2799
|
-
typeof this.#rawData[deviceUUID] !== 'object' ||
|
|
2795
|
+
typeof this.#rawData?.[deviceUUID] !== 'object' ||
|
|
2800
2796
|
typeof values !== 'object' ||
|
|
2801
|
-
typeof this.#connections[this.#rawData[deviceUUID]?.connection] !== 'object'
|
|
2797
|
+
typeof this.#connections[this.#rawData?.[deviceUUID]?.connection] !== 'object'
|
|
2802
2798
|
) {
|
|
2803
2799
|
values = {};
|
|
2804
2800
|
}
|
|
@@ -2810,94 +2806,84 @@ export default class NestAccfactory {
|
|
|
2810
2806
|
values[key] = undefined;
|
|
2811
2807
|
|
|
2812
2808
|
if (
|
|
2813
|
-
this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.REST &&
|
|
2809
|
+
this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.REST &&
|
|
2814
2810
|
key === 'camera_snapshot' &&
|
|
2815
|
-
deviceUUID.startsWith('quartz.') === true
|
|
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 !== ''
|
|
2816
2814
|
) {
|
|
2817
2815
|
// Attempt to retrieve snapshot from camera via REST API
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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
2828
|
},
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
// if (typeof keyValue keyValue !== '')
|
|
2834
|
-
/* (url =
|
|
2835
|
-
this.#rawData[deviceUUID].value.nexus_api_http_server_url +
|
|
2836
|
-
'/event_snapshot/' +
|
|
2837
|
-
deviceUUID.split('.')[1] +
|
|
2838
|
-
'/' +
|
|
2839
|
-
id +
|
|
2840
|
-
'?crop_type=timeline&cachebuster=' +
|
|
2841
|
-
Math.floor(Date.now() / 1000)), */
|
|
2842
|
-
|
|
2843
|
-
await axios(request)
|
|
2844
|
-
.then((response) => {
|
|
2845
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
2846
|
-
throw new Error('REST API camera snapshot failed with error');
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
values[key] = response.data;
|
|
2829
|
+
)
|
|
2830
|
+
.then((response) => response.arrayBuffer())
|
|
2831
|
+
.then((data) => {
|
|
2832
|
+
values[key] = Buffer.from(data);
|
|
2850
2833
|
})
|
|
2851
2834
|
.catch((error) => {
|
|
2852
|
-
this?.log?.debug
|
|
2835
|
+
if (error?.name !== 'TimeoutError' && this?.log?.debug) {
|
|
2853
2836
|
this.log.debug('REST API camera snapshot failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2837
|
+
}
|
|
2854
2838
|
});
|
|
2855
2839
|
}
|
|
2856
2840
|
|
|
2857
2841
|
if (
|
|
2858
|
-
this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.PROTOBUF &&
|
|
2842
|
+
this.#rawData?.[deviceUUID]?.source === NestAccfactory.DataSource.PROTOBUF &&
|
|
2859
2843
|
this.#connections[this.#rawData[deviceUUID].connection].protobufRoot !== null &&
|
|
2860
2844
|
this.#rawData[deviceUUID]?.value?.device_identity?.vendorProductId !== undefined &&
|
|
2861
2845
|
key === 'camera_snapshot'
|
|
2862
2846
|
) {
|
|
2863
|
-
// Attempt to retrieve snapshot from camera via
|
|
2847
|
+
// Attempt to retrieve snapshot from camera via Protobuf API
|
|
2864
2848
|
// First, request to get snapshot url image updated
|
|
2865
|
-
let commandResponse = await this.#protobufCommand(deviceUUID,
|
|
2866
|
-
{
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
type_url: 'type.nestlabs.com/nest.trait.product.camera.UploadLiveImageTrait.UploadLiveImageRequest',
|
|
2870
|
-
value: {},
|
|
2871
|
-
},
|
|
2849
|
+
let commandResponse = await this.#protobufCommand(this.#rawData[deviceUUID].connection, 'ResourceApi', 'SendCommand', {
|
|
2850
|
+
resourceRequest: {
|
|
2851
|
+
resourceId: deviceUUID,
|
|
2852
|
+
requestId: crypto.randomUUID(),
|
|
2872
2853
|
},
|
|
2873
|
-
|
|
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
|
+
});
|
|
2874
2864
|
|
|
2875
|
-
if (
|
|
2865
|
+
if (
|
|
2866
|
+
commandResponse?.sendCommandResponse?.[0]?.traitOperations?.[0]?.progress === 'COMPLETE' &&
|
|
2867
|
+
typeof this.#rawData?.[deviceUUID]?.value?.upload_live_image?.liveImageUrl === 'string' &&
|
|
2868
|
+
this.#rawData[deviceUUID].value.upload_live_image.liveImageUrl !== ''
|
|
2869
|
+
) {
|
|
2876
2870
|
// Snapshot url image has beeen updated, so no retrieve it
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
url: this.#rawData[deviceUUID].value.upload_live_image.liveImageUrl,
|
|
2871
|
+
await fetchWrapper('get', this.#rawData[deviceUUID].value.upload_live_image.liveImageUrl, {
|
|
2872
|
+
referer: 'https://' + this.#connections[this.#rawData[deviceUUID].connection].referer,
|
|
2880
2873
|
headers: {
|
|
2881
2874
|
'User-Agent': USERAGENT,
|
|
2882
|
-
|
|
2883
|
-
[this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.key]:
|
|
2884
|
-
this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.value +
|
|
2885
|
-
this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.token,
|
|
2875
|
+
Authorization: 'Basic ' + this.#connections[this.#rawData[deviceUUID].connection].token,
|
|
2886
2876
|
},
|
|
2887
|
-
responseType: 'arraybuffer',
|
|
2888
2877
|
timeout: 3000,
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
|
-
.then((
|
|
2892
|
-
|
|
2893
|
-
throw new Error('protobuf API camera snapshot failed with error');
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
values[key] = response.data;
|
|
2878
|
+
})
|
|
2879
|
+
.then((response) => response.arrayBuffer())
|
|
2880
|
+
.then((data) => {
|
|
2881
|
+
values[key] = Buffer.from(data);
|
|
2897
2882
|
})
|
|
2898
2883
|
.catch((error) => {
|
|
2899
|
-
this?.log?.debug
|
|
2900
|
-
this.log.debug('
|
|
2884
|
+
if (error?.name !== 'TimeoutError' && this?.log?.debug) {
|
|
2885
|
+
this.log.debug('Protobuf API camera snapshot failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2886
|
+
}
|
|
2901
2887
|
});
|
|
2902
2888
|
}
|
|
2903
2889
|
}
|
|
@@ -2908,124 +2894,118 @@ export default class NestAccfactory {
|
|
|
2908
2894
|
this.#eventEmitter.emit(HomeKitDevice.GET + '->' + deviceUUID, values);
|
|
2909
2895
|
}
|
|
2910
2896
|
|
|
2911
|
-
async #getWeatherData(
|
|
2897
|
+
async #getWeatherData(connectionUUID, deviceUUID, latitude, longitude) {
|
|
2912
2898
|
let weatherData = {};
|
|
2913
2899
|
if (typeof this.#rawData[deviceUUID]?.value?.weather === 'object') {
|
|
2914
|
-
weatherData = this.#rawData[deviceUUID]
|
|
2900
|
+
weatherData = this.#rawData?.[deviceUUID]?.value.weather;
|
|
2915
2901
|
}
|
|
2916
2902
|
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
.then((response) => {
|
|
2928
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
2929
|
-
throw new Error('REST API failed to retireve weather details');
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
if (typeof response.data[latitude + ',' + longitude].current === 'object') {
|
|
2903
|
+
if (typeof this.#connections?.[connectionUUID].weather_url === 'string' && this.#connections[connectionUUID].weather_url !== '') {
|
|
2904
|
+
await fetchWrapper('get', this.#connections[connectionUUID].weather_url + latitude + ',' + longitude, {
|
|
2905
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
2906
|
+
headers: {
|
|
2907
|
+
'User-Agent': USERAGENT,
|
|
2908
|
+
},
|
|
2909
|
+
timeout: NESTAPITIMEOUT,
|
|
2910
|
+
})
|
|
2911
|
+
.then((response) => response.json())
|
|
2912
|
+
.then((data) => {
|
|
2933
2913
|
// Store the lat/long details in the weather data object
|
|
2934
2914
|
weatherData.latitude = latitude;
|
|
2935
2915
|
weatherData.longitude = longitude;
|
|
2936
2916
|
|
|
2937
|
-
// Update weather data
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2917
|
+
// Update weather data
|
|
2918
|
+
if (data?.[latitude + ',' + longitude]?.current !== undefined) {
|
|
2919
|
+
weatherData.current_temperature = adjustTemperature(data[latitude + ',' + longitude].current.temp_c, 'C', 'C', false);
|
|
2920
|
+
weatherData.current_humidity = data[latitude + ',' + longitude].current.humidity;
|
|
2921
|
+
weatherData.condition = data[latitude + ',' + longitude].current.condition;
|
|
2922
|
+
weatherData.wind_direction = data[latitude + ',' + longitude].current.wind_dir;
|
|
2923
|
+
weatherData.wind_speed = data[latitude + ',' + longitude].current.wind_mph * 1.609344; // convert to km/h
|
|
2924
|
+
weatherData.sunrise = data[latitude + ',' + longitude].current.sunrise;
|
|
2925
|
+
weatherData.sunset = data[latitude + ',' + longitude].current.sunset;
|
|
2926
|
+
}
|
|
2927
|
+
weatherData.station =
|
|
2928
|
+
data[latitude + ',' + longitude]?.location?.short_name !== undefined
|
|
2929
|
+
? data[latitude + ',' + longitude].location.short_name
|
|
2930
|
+
: '';
|
|
2931
|
+
weatherData.forecast =
|
|
2932
|
+
data[latitude + ',' + longitude]?.forecast?.daily?.[0]?.condition !== undefined
|
|
2933
|
+
? data[latitude + ',' + longitude].forecast.daily[0].condition
|
|
2934
|
+
: '';
|
|
2935
|
+
})
|
|
2936
|
+
.catch((error) => {
|
|
2937
|
+
if (error?.name !== 'TimeoutError' && this?.log?.debug) {
|
|
2938
|
+
this.log.debug('REST API failed to retrieve weather details for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2953
2943
|
return weatherData;
|
|
2954
2944
|
}
|
|
2955
2945
|
|
|
2956
|
-
async #protobufCommand(
|
|
2946
|
+
async #protobufCommand(connectionUUID, service, command, values) {
|
|
2957
2947
|
if (
|
|
2958
|
-
|
|
2959
|
-
typeof
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2948
|
+
this.#connections?.[connectionUUID]?.protobufRoot === null ||
|
|
2949
|
+
typeof service !== 'string' ||
|
|
2950
|
+
service === '' ||
|
|
2951
|
+
typeof command !== 'string' ||
|
|
2952
|
+
command === '' ||
|
|
2953
|
+
typeof values !== 'object'
|
|
2963
2954
|
) {
|
|
2964
2955
|
return;
|
|
2965
2956
|
}
|
|
2966
2957
|
|
|
2967
|
-
|
|
2968
|
-
|
|
2958
|
+
const encodeValues = (object) => {
|
|
2959
|
+
if (typeof object === 'object' && object !== null) {
|
|
2960
|
+
if ('type_url' in object && 'value' in object) {
|
|
2961
|
+
// We have a type_url and value object at this same level, we'll treat this a trait requiring encoding
|
|
2962
|
+
let TraitMap = this.#connections[connectionUUID].protobufRoot.lookup(object.type_url.split('/')[1]);
|
|
2963
|
+
if (TraitMap !== null) {
|
|
2964
|
+
object.value = TraitMap.encode(TraitMap.fromObject(object.value)).finish();
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2969
2967
|
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
}
|
|
2976
|
-
resourceCommands: commands,
|
|
2968
|
+
for (let key in object) {
|
|
2969
|
+
if (object?.[key] !== undefined) {
|
|
2970
|
+
encodeValues(object[key]);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2977
2974
|
};
|
|
2978
2975
|
|
|
2979
|
-
//
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
command.command.value = trait.encode(trait.fromObject(command.command.value)).finish();
|
|
2984
|
-
}
|
|
2985
|
-
});
|
|
2976
|
+
// Attempt to retrieve both 'Request' and 'Reponse' traits for the associated service and command
|
|
2977
|
+
let TraitMapRequest = this.#connections[connectionUUID].protobufRoot.lookup('nestlabs.gateway.v1.' + command + 'Request');
|
|
2978
|
+
let TraitMapResponse = this.#connections[connectionUUID].protobufRoot.lookup('nestlabs.gateway.v1.' + command + 'Response');
|
|
2979
|
+
let commandResponse = undefined;
|
|
2986
2980
|
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
if (TraitMap !== null) {
|
|
2991
|
-
encodedData = TraitMap.encode(TraitMap.fromObject(protobufElement)).finish();
|
|
2992
|
-
}
|
|
2981
|
+
if (TraitMapRequest !== null && TraitMapResponse !== null) {
|
|
2982
|
+
// Encode any trait values in our passed in object
|
|
2983
|
+
encodeValues(values);
|
|
2993
2984
|
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
2985
|
+
let encodedData = TraitMapRequest.encode(TraitMapRequest.fromObject(values)).finish();
|
|
2986
|
+
await fetchWrapper(
|
|
2987
|
+
'post',
|
|
2988
|
+
'https://' + this.#connections[connectionUUID].protobufAPIHost + '/nestlabs.gateway.v1.' + service + '/' + command,
|
|
2989
|
+
{
|
|
2990
|
+
headers: {
|
|
2991
|
+
referer: 'https://' + this.#connections[connectionUUID].referer,
|
|
2992
|
+
'User-Agent': USERAGENT,
|
|
2993
|
+
Authorization: 'Basic ' + this.#connections[connectionUUID].token,
|
|
2994
|
+
'Content-Type': 'application/x-protobuf',
|
|
2995
|
+
'X-Accept-Content-Transfer-Encoding': 'binary',
|
|
2996
|
+
'X-Accept-Response-Streaming': 'true',
|
|
2997
|
+
},
|
|
3007
2998
|
},
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
.then((response) => {
|
|
3014
|
-
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
3015
|
-
throw new Error('protobuf command send failed with error');
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
commandResponse = this.#connections[this.#rawData[deviceUUID].connection].protobufRoot
|
|
3019
|
-
.lookup('nestlabs.gateway.v1.ResourceCommandResponseFromAPI')
|
|
3020
|
-
.decode(response.data)
|
|
3021
|
-
.toJSON();
|
|
2999
|
+
encodedData,
|
|
3000
|
+
)
|
|
3001
|
+
.then((response) => response.bytes())
|
|
3002
|
+
.then((data) => {
|
|
3003
|
+
commandResponse = TraitMapResponse.decode(data).toJSON();
|
|
3022
3004
|
})
|
|
3023
3005
|
.catch((error) => {
|
|
3024
|
-
this?.log?.debug &&
|
|
3025
|
-
this.log.debug('protobuf command send failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
3006
|
+
this?.log?.debug && this.log.debug('Protobuf gateway service command failed with error. Error was "%s"', error?.code);
|
|
3026
3007
|
});
|
|
3027
3008
|
}
|
|
3028
|
-
|
|
3029
3009
|
return commandResponse;
|
|
3030
3010
|
}
|
|
3031
3011
|
}
|
|
@@ -3063,10 +3043,12 @@ function makeHomeKitName(nameToMakeValid) {
|
|
|
3063
3043
|
// Strip invalid characters to meet HomeKit naming requirements
|
|
3064
3044
|
// Ensure only letters or numbers are at the beginning AND/OR end of string
|
|
3065
3045
|
// Matches against uni-code characters
|
|
3066
|
-
return nameToMakeValid
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3046
|
+
return typeof nameToMakeValid === 'string'
|
|
3047
|
+
? nameToMakeValid
|
|
3048
|
+
.replace(/[^\p{L}\p{N}\p{Z}\u2019.,-]/gu, '')
|
|
3049
|
+
.replace(/^[^\p{L}\p{N}]*/gu, '')
|
|
3050
|
+
.replace(/[^\p{L}\p{N}]+$/gu, '')
|
|
3051
|
+
: nameToMakeValid;
|
|
3070
3052
|
}
|
|
3071
3053
|
|
|
3072
3054
|
function crc24(valueToHash) {
|
|
@@ -3110,3 +3092,31 @@ function scaleValue(value, sourceRangeMin, sourceRangeMax, targetRangeMin, targe
|
|
|
3110
3092
|
}
|
|
3111
3093
|
return ((value - sourceRangeMin) * (targetRangeMax - targetRangeMin)) / (sourceRangeMax - sourceRangeMin) + targetRangeMin;
|
|
3112
3094
|
}
|
|
3095
|
+
|
|
3096
|
+
async function fetchWrapper(method, url, options, data) {
|
|
3097
|
+
if ((method !== 'get' && method !== 'post') || typeof url !== 'string' || url === '' || typeof options !== 'object') {
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
if (typeof options?.timeout === 'number' && options?.timeout > 0) {
|
|
3102
|
+
// If a timeout is specified in the options, setup here
|
|
3103
|
+
// eslint-disable-next-line no-undef
|
|
3104
|
+
options.signal = AbortSignal.timeout(options.timeout);
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
options.method = method; // Set the HTTP method to use
|
|
3108
|
+
|
|
3109
|
+
if (method === 'post' && typeof data !== undefined) {
|
|
3110
|
+
// Doing a HTTP post, so include the data in the body
|
|
3111
|
+
options.body = data;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// eslint-disable-next-line no-undef
|
|
3115
|
+
let response = await fetch(url, options);
|
|
3116
|
+
if (response.ok === false) {
|
|
3117
|
+
let error = new Error(response.statusText);
|
|
3118
|
+
error.code = response.status;
|
|
3119
|
+
throw error;
|
|
3120
|
+
}
|
|
3121
|
+
return response;
|
|
3122
|
+
}
|