homebridge-nest-accfactory 0.0.4-a
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 +27 -0
- package/LICENSE +176 -0
- package/README.md +121 -0
- package/config.schema.json +107 -0
- package/dist/HomeKitDevice.js +441 -0
- package/dist/HomeKitHistory.js +2835 -0
- package/dist/camera.js +1276 -0
- package/dist/doorbell.js +122 -0
- package/dist/index.js +35 -0
- package/dist/nexustalk.js +741 -0
- package/dist/protect.js +240 -0
- package/dist/protobuf/google/rpc/status.proto +91 -0
- package/dist/protobuf/google/rpc/stream_body.proto +26 -0
- package/dist/protobuf/google/trait/product/camera.proto +53 -0
- package/dist/protobuf/googlehome/foyer.proto +208 -0
- package/dist/protobuf/nest/messages.proto +8 -0
- package/dist/protobuf/nest/services/apigateway.proto +107 -0
- package/dist/protobuf/nest/trait/audio.proto +7 -0
- package/dist/protobuf/nest/trait/cellular.proto +313 -0
- package/dist/protobuf/nest/trait/debug.proto +37 -0
- package/dist/protobuf/nest/trait/detector.proto +41 -0
- package/dist/protobuf/nest/trait/diagnostics.proto +87 -0
- package/dist/protobuf/nest/trait/firmware.proto +221 -0
- package/dist/protobuf/nest/trait/guest.proto +105 -0
- package/dist/protobuf/nest/trait/history.proto +345 -0
- package/dist/protobuf/nest/trait/humanlibrary.proto +19 -0
- package/dist/protobuf/nest/trait/hvac.proto +1353 -0
- package/dist/protobuf/nest/trait/input.proto +29 -0
- package/dist/protobuf/nest/trait/lighting.proto +61 -0
- package/dist/protobuf/nest/trait/located.proto +193 -0
- package/dist/protobuf/nest/trait/media.proto +68 -0
- package/dist/protobuf/nest/trait/network.proto +352 -0
- package/dist/protobuf/nest/trait/occupancy.proto +373 -0
- package/dist/protobuf/nest/trait/olive.proto +15 -0
- package/dist/protobuf/nest/trait/pairing.proto +85 -0
- package/dist/protobuf/nest/trait/product/camera.proto +283 -0
- package/dist/protobuf/nest/trait/product/detect.proto +67 -0
- package/dist/protobuf/nest/trait/product/doorbell.proto +18 -0
- package/dist/protobuf/nest/trait/product/guard.proto +59 -0
- package/dist/protobuf/nest/trait/product/protect.proto +344 -0
- package/dist/protobuf/nest/trait/promonitoring.proto +14 -0
- package/dist/protobuf/nest/trait/resourcedirectory.proto +32 -0
- package/dist/protobuf/nest/trait/safety.proto +119 -0
- package/dist/protobuf/nest/trait/security.proto +516 -0
- package/dist/protobuf/nest/trait/selftest.proto +78 -0
- package/dist/protobuf/nest/trait/sensor.proto +291 -0
- package/dist/protobuf/nest/trait/service.proto +46 -0
- package/dist/protobuf/nest/trait/structure.proto +85 -0
- package/dist/protobuf/nest/trait/system.proto +51 -0
- package/dist/protobuf/nest/trait/test.proto +15 -0
- package/dist/protobuf/nest/trait/ui.proto +65 -0
- package/dist/protobuf/nest/trait/user.proto +98 -0
- package/dist/protobuf/nest/trait/voiceassistant.proto +30 -0
- package/dist/protobuf/nestlabs/eventingapi/v1.proto +83 -0
- package/dist/protobuf/nestlabs/gateway/v1.proto +273 -0
- package/dist/protobuf/nestlabs/gateway/v2.proto +96 -0
- package/dist/protobuf/nestlabs/history/v1.proto +73 -0
- package/dist/protobuf/root.proto +64 -0
- package/dist/protobuf/wdl-event-importance.proto +11 -0
- package/dist/protobuf/wdl.proto +450 -0
- package/dist/protobuf/weave/common.proto +144 -0
- package/dist/protobuf/weave/trait/audio.proto +12 -0
- package/dist/protobuf/weave/trait/auth.proto +22 -0
- package/dist/protobuf/weave/trait/description.proto +32 -0
- package/dist/protobuf/weave/trait/heartbeat.proto +38 -0
- package/dist/protobuf/weave/trait/locale.proto +20 -0
- package/dist/protobuf/weave/trait/network.proto +24 -0
- package/dist/protobuf/weave/trait/pairing.proto +8 -0
- package/dist/protobuf/weave/trait/peerdevices.proto +18 -0
- package/dist/protobuf/weave/trait/power.proto +86 -0
- package/dist/protobuf/weave/trait/schedule.proto +76 -0
- package/dist/protobuf/weave/trait/security.proto +343 -0
- package/dist/protobuf/weave/trait/telemetry/tunnel.proto +37 -0
- package/dist/protobuf/weave/trait/time.proto +16 -0
- package/dist/res/Nest_camera_connecting.h264 +0 -0
- package/dist/res/Nest_camera_connecting.jpg +0 -0
- package/dist/res/Nest_camera_off.h264 +0 -0
- package/dist/res/Nest_camera_off.jpg +0 -0
- package/dist/res/Nest_camera_offline.h264 +0 -0
- package/dist/res/Nest_camera_offline.jpg +0 -0
- package/dist/res/Nest_camera_transfer.jpg +0 -0
- package/dist/streamer.js +344 -0
- package/dist/system.js +3112 -0
- package/dist/tempsensor.js +99 -0
- package/dist/thermostat.js +1026 -0
- package/dist/weather.js +205 -0
- package/dist/webrtc.js +55 -0
- package/package.json +66 -0
package/dist/system.js
ADDED
|
@@ -0,0 +1,3112 @@
|
|
|
1
|
+
// Nest System communications
|
|
2
|
+
// Part of homebridge-nest-accfactory
|
|
3
|
+
//
|
|
4
|
+
// Code version 7/9/2024
|
|
5
|
+
// Mark Hulskamp
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// Define external module requirements
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
import protobuf from 'protobufjs';
|
|
11
|
+
|
|
12
|
+
// Define nodejs module requirements
|
|
13
|
+
import EventEmitter from 'node:events';
|
|
14
|
+
import { Buffer } from 'node:buffer';
|
|
15
|
+
import { setInterval, clearInterval, setTimeout } from 'node:timers';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import crypto from 'node:crypto';
|
|
19
|
+
import process from 'node:process';
|
|
20
|
+
import child_process from 'node:child_process';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
// Import our modules
|
|
24
|
+
import HomeKitDevice from './HomeKitDevice.js';
|
|
25
|
+
import NestCamera from './camera.js';
|
|
26
|
+
import NestDoorbell from './doorbell.js';
|
|
27
|
+
import NestProtect from './protect.js';
|
|
28
|
+
import NestTemperatureSensor from './tempsensor.js';
|
|
29
|
+
import NestWeather from './weather.js';
|
|
30
|
+
import NestThermostat from './thermostat.js';
|
|
31
|
+
|
|
32
|
+
const CAMERAALERTPOLLING = 2000; // Camera alerts polling timer
|
|
33
|
+
const CAMERAZONEPOLLING = 30000; // Camera zones changes polling timer
|
|
34
|
+
const WEATHERPOLLING = 300000; // Weather data polling timer
|
|
35
|
+
const NESTAPITIMEOUT = 10000; // Nest API timeout
|
|
36
|
+
const USERAGENT = 'Nest/5.78.0 (iOScom.nestlabs.jasper.release) os=18.0'; // User Agent string
|
|
37
|
+
const FFMPEGVERSION = '6.0'; // Minimum version of ffmpeg we require
|
|
38
|
+
|
|
39
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
40
|
+
|
|
41
|
+
// We handle the connections to Nest/Google
|
|
42
|
+
// Perform device management (additions/removals/updates)
|
|
43
|
+
export default class NestAccfactory {
|
|
44
|
+
static DeviceType = {
|
|
45
|
+
THERMOSTAT: 'thermostat',
|
|
46
|
+
TEMPSENSOR: 'temperature',
|
|
47
|
+
SMOKESENSOR: 'protect',
|
|
48
|
+
CAMERA: 'camera',
|
|
49
|
+
DOORBELL: 'doorbell',
|
|
50
|
+
WEATHER: 'weather',
|
|
51
|
+
LOCK: 'lock', // yet to implement
|
|
52
|
+
ALARM: 'alarm', // yet to implement
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
static DataSource = {
|
|
56
|
+
REST: 'REST', // From the REST API
|
|
57
|
+
PROTOBUF: 'PROTOBUF', // From the protobuf API
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
static GoogleConnection = 'google'; // Google account connection
|
|
61
|
+
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
|
+
|
|
65
|
+
cachedAccessories = []; // Track restored cached accessories
|
|
66
|
+
|
|
67
|
+
// Internal data only for this class
|
|
68
|
+
#connections = {}; // Array of confirmed connections, indexed by type
|
|
69
|
+
#rawData = {}; // Cached copy of data from both Rest and Protobuf APIs
|
|
70
|
+
#eventEmitter = new EventEmitter(); // Used for object messaging from this platform
|
|
71
|
+
|
|
72
|
+
constructor(log, config, api) {
|
|
73
|
+
this.config = config;
|
|
74
|
+
this.log = log;
|
|
75
|
+
this.api = api;
|
|
76
|
+
|
|
77
|
+
// 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
|
+
|
|
84
|
+
if (typeof this.config?.google !== 'object') {
|
|
85
|
+
this.config.google = {};
|
|
86
|
+
}
|
|
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
|
+
|
|
91
|
+
if (typeof this.config?.options !== 'object') {
|
|
92
|
+
this.config.options = {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.config.options.eveHistory = typeof this.config.options?.eveHistory === 'boolean' ? this.config.options.eveHistory : false;
|
|
96
|
+
this.config.options.elevation = typeof this.config.options?.elevation === 'number' ? this.config.options.elevation : 0;
|
|
97
|
+
this.config.options.weather = typeof this.config.options?.weather === 'boolean' ? this.config.options.weather : false;
|
|
98
|
+
this.config.options.hksv = typeof this.config.options?.hksv === 'boolean' ? this.config.options.hksv : false;
|
|
99
|
+
|
|
100
|
+
// Get configuration for max number of concurrent 'live view' streams. For HomeKit Secure Video, this will always be 1
|
|
101
|
+
this.config.options.maxStreams =
|
|
102
|
+
typeof this.config.options?.maxStreams === 'number' && this.deviceData?.hksv === false
|
|
103
|
+
? this.config.options.maxStreams
|
|
104
|
+
: this.deviceData?.hksv === true
|
|
105
|
+
? 1
|
|
106
|
+
: 2;
|
|
107
|
+
|
|
108
|
+
// Check if a ffmpeg binary exists in current path OR the specific path via configuration
|
|
109
|
+
// If using HomeBridge, the default path will be where the Homebridge user folder is, otherwise the current directory
|
|
110
|
+
this.config.options.ffmpeg = {};
|
|
111
|
+
this.config.options.ffmpeg['path'] =
|
|
112
|
+
typeof this.config.options?.ffmpegPath === 'string' && this.config.options.ffmpegPath !== ''
|
|
113
|
+
? this.config.options.ffmpegPath
|
|
114
|
+
: typeof api?.user?.storagePath === 'function'
|
|
115
|
+
? api.user.storagePath()
|
|
116
|
+
: __dirname;
|
|
117
|
+
|
|
118
|
+
this.config.options.ffmpeg['version'] = undefined;
|
|
119
|
+
this.config.options.ffmpeg.libspeex = false;
|
|
120
|
+
this.config.options.ffmpeg.libx264 = false;
|
|
121
|
+
this.config.options.ffmpeg.libfdk_aac = false;
|
|
122
|
+
|
|
123
|
+
if (fs.existsSync(path.resolve(this.config.options.ffmpeg.path + '/ffmpeg')) === false) {
|
|
124
|
+
if (this?.log?.warn) {
|
|
125
|
+
this.log.warn('No ffmpeg binary found in "%s"', this.config.options.ffmpeg.path);
|
|
126
|
+
this.log.warn('Stream video/recording from camera/doorbells will be unavailable');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If we flag ffmpegPath as undefined, no video streaming/record support enabled for camers/doorbells
|
|
130
|
+
this.config.options.ffmpeg.path = undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (fs.existsSync(path.resolve(this.config.options.ffmpeg.path + '/ffmpeg')) === true) {
|
|
134
|
+
let ffmpegProcess = child_process.spawnSync(path.resolve(this.config.options.ffmpeg.path + '/ffmpeg'), ['-version'], {
|
|
135
|
+
env: process.env,
|
|
136
|
+
});
|
|
137
|
+
if (ffmpegProcess.stdout !== null) {
|
|
138
|
+
// Determine ffmpeg version
|
|
139
|
+
this.config.options.ffmpeg.version = ffmpegProcess.stdout
|
|
140
|
+
.toString()
|
|
141
|
+
.match(/(?:ffmpeg version:(\d+)\.)?(?:(\d+)\.)?(?:(\d+)\.\d+)(.*?)/gim)[0];
|
|
142
|
+
|
|
143
|
+
// Determine what libraries ffmpeg is compiled with
|
|
144
|
+
this.config.options.ffmpeg.libspeex = ffmpegProcess.stdout.toString().includes('--enable-libspeex') === true;
|
|
145
|
+
this.config.options.ffmpeg.libx264 = ffmpegProcess.stdout.toString().includes('--enable-libx264') === true;
|
|
146
|
+
this.config.options.ffmpeg.libfdk_aac = ffmpegProcess.stdout.toString().includes('--enable-libfdk-aac') === true;
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
this.config.options.ffmpeg.version.replace(/\./gi, '') < parseFloat(FFMPEGVERSION.toString().replace(/\./gi, '')) ||
|
|
150
|
+
this.config.options.ffmpeg.libspeex === false ||
|
|
151
|
+
this.config.options.ffmpeg.libx264 === false ||
|
|
152
|
+
this.config.options.ffmpeg.libfdk_aac === false
|
|
153
|
+
) {
|
|
154
|
+
this?.log?.warn &&
|
|
155
|
+
this.log.warn('ffmpeg binary in "%s" does not meet the minimum support requirements', this.config.options.ffmpeg.path);
|
|
156
|
+
if (this.config.options.ffmpeg.version.replace(/\./gi, '') < parseFloat(FFMPEGVERSION.toString().replace(/\./gi, ''))) {
|
|
157
|
+
this?.log?.warn &&
|
|
158
|
+
this.log.warn(
|
|
159
|
+
'Minimum binary version is "%s", however the installed version is "%s"',
|
|
160
|
+
FFMPEGVERSION,
|
|
161
|
+
this.config.options.ffmpeg.version,
|
|
162
|
+
);
|
|
163
|
+
this?.log?.warn && this.log.warn('Stream video/recording from camera/doorbells will be unavailable');
|
|
164
|
+
|
|
165
|
+
this.config.options.ffmpeg.path = undefined; // No ffmpeg since below min version
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
this.config.options.ffmpeg.libspeex === false &&
|
|
169
|
+
(this.config.options.ffmpeg.libx264 === true && this.config.options.ffmpeg.libfdk_aac) === true
|
|
170
|
+
) {
|
|
171
|
+
this?.log?.warn && this.log.warn('Missing libspeex in ffmpeg binary, two-way audio on camera/doorbells will be unavailable');
|
|
172
|
+
}
|
|
173
|
+
if (this.config.options.ffmpeg.libx264 === true && this.config.options.ffmpeg.libfdk_aac === false) {
|
|
174
|
+
this?.log?.warn && this.log.warn('Missing libfdk_aac in ffmpeg binary, audio from camera/doorbells will be unavailable');
|
|
175
|
+
}
|
|
176
|
+
if (this.config.options.ffmpeg.libx264 === false) {
|
|
177
|
+
this?.log?.warn &&
|
|
178
|
+
this.log.warn('Missing libx264 in ffmpeg binary, stream video/recording from camera/doorbells will be unavailable');
|
|
179
|
+
|
|
180
|
+
this.config.options.ffmpeg.path = undefined; // No ffmpeg since we do not have all the required libraries
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
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
|
+
if (this.api instanceof EventEmitter === true) {
|
|
193
|
+
this.api.on('didFinishLaunching', async () => {
|
|
194
|
+
// We got notified that Homebridge has finished loading, so we are ready to process
|
|
195
|
+
this.discoverDevices();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.api.on('shutdown', async () => {
|
|
199
|
+
// We got notified that Homebridge is shutting down. Perform cleanup??
|
|
200
|
+
this.#eventEmitter.removeAllListeners(HomeKitDevice.SET);
|
|
201
|
+
this.#eventEmitter.removeAllListeners(HomeKitDevice.GET);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
configureAccessory(accessory) {
|
|
207
|
+
// This gets called from HomeBridge each time it restores an accessory from its cache
|
|
208
|
+
this?.log?.info && this.log.info('Loading accessory from cache:', accessory.displayName);
|
|
209
|
+
|
|
210
|
+
// add the restored accessory to the accessories cache, so we can track if it has already been registered
|
|
211
|
+
this.cachedAccessories.push(accessory);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
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
|
+
// Setup event listeners for set/get calls from devices
|
|
229
|
+
this.#eventEmitter.addListener(HomeKitDevice.SET, (deviceUUID, values) => this.#set(deviceUUID, values));
|
|
230
|
+
this.#eventEmitter.addListener(HomeKitDevice.GET, (deviceUUID, values) => this.#get(deviceUUID, values));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async #connect() {
|
|
234
|
+
if (
|
|
235
|
+
typeof this.config?.google === 'object' &&
|
|
236
|
+
typeof this.config?.google?.issuetoken === 'string' &&
|
|
237
|
+
this.config?.google?.issuetoken !== '' &&
|
|
238
|
+
typeof this.config?.google?.cookie === 'string' &&
|
|
239
|
+
this.config?.google?.cookie !== ''
|
|
240
|
+
) {
|
|
241
|
+
let referer = 'home.nest.com'; // Which host is 'actually' doing the request
|
|
242
|
+
let restAPIHost = 'home.nest.com'; // Root URL for Nest system REST API
|
|
243
|
+
let cameraAPIHost = 'camera.home.nest.com'; // Root URL for Camera system API
|
|
244
|
+
let protobufAPIHost = 'grpc-web.production.nest.com'; // Root URL for Protobuf API
|
|
245
|
+
|
|
246
|
+
if (this.config?.google.fieldTest === true) {
|
|
247
|
+
// FieldTest mode support enabled in configuration, so update default endpoints
|
|
248
|
+
// This is all 'untested'
|
|
249
|
+
this?.log?.info && this.log.info('Using FieldTest API endpoints for Google account');
|
|
250
|
+
|
|
251
|
+
referer = 'home.ft.nest.com'; // Which host is 'actually' doing the request
|
|
252
|
+
restAPIHost = 'home.ft.nest.com'; // Root FT URL for Nest system REST API
|
|
253
|
+
cameraAPIHost = 'camera.home.ft.nest.com'; // Root FT URL for Camera system API
|
|
254
|
+
protobufAPIHost = 'grpc-web.ft.nest.com'; // Root FT URL for Protobuf API
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Google cookie method as refresh token method no longer supported by Google since October 2022
|
|
258
|
+
// Instructions from homebridge_nest or homebridge_nest_cam to obtain this
|
|
259
|
+
this?.log?.info && this.log.info('Performing Google account authorisation');
|
|
260
|
+
|
|
261
|
+
let request = {
|
|
262
|
+
method: 'get',
|
|
263
|
+
url: this.config.google.issuetoken,
|
|
264
|
+
headers: {
|
|
265
|
+
referer: 'https://accounts.google.com/o/oauth2/iframe',
|
|
266
|
+
'User-Agent': USERAGENT,
|
|
267
|
+
cookie: this.config.google.cookie,
|
|
268
|
+
'Sec-Fetch-Mode': 'cors',
|
|
269
|
+
'X-Requested-With': 'XmlHttpRequest',
|
|
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;
|
|
278
|
+
|
|
279
|
+
let request = {
|
|
280
|
+
method: 'post',
|
|
281
|
+
url: 'https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt',
|
|
282
|
+
headers: {
|
|
283
|
+
referer: 'https://' + referer,
|
|
284
|
+
'User-Agent': USERAGENT,
|
|
285
|
+
Authorization: 'Bearer ' + response.data.access_token,
|
|
286
|
+
},
|
|
287
|
+
data:
|
|
288
|
+
'embed_google_oauth_access_token=true&expire_after=3600s&google_oauth_access_token=' +
|
|
289
|
+
response.data.access_token +
|
|
290
|
+
'&policy_id=authproxy-oauth-policy',
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
await axios(request).then(async (response) => {
|
|
294
|
+
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
295
|
+
throw new Error('Google Camera API Token get failed with error');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let googleToken = response.data.jwt;
|
|
299
|
+
let tokenExpire = Math.floor(new Date(response.data.claims.expirationTime).valueOf() / 1000); // Token expiry, should be 1hr
|
|
300
|
+
|
|
301
|
+
let request = {
|
|
302
|
+
method: 'get',
|
|
303
|
+
url: 'https://' + restAPIHost + '/session',
|
|
304
|
+
headers: {
|
|
305
|
+
referer: 'https://' + referer,
|
|
306
|
+
'User-Agent': USERAGENT,
|
|
307
|
+
Authorization: 'Basic ' + googleToken,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
await axios(request).then(async (response) => {
|
|
312
|
+
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
313
|
+
throw new Error('Nest Session API get failed with error');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this?.log?.success && this.log.success('Successfully authorised using Google account');
|
|
317
|
+
|
|
318
|
+
// Store successful connection details
|
|
319
|
+
this.#connections['google'] = {
|
|
320
|
+
type: 'google',
|
|
321
|
+
referer: referer,
|
|
322
|
+
restAPIHost: restAPIHost,
|
|
323
|
+
cameraAPIHost: cameraAPIHost,
|
|
324
|
+
protobufAPIHost: protobufAPIHost,
|
|
325
|
+
userID: response.data.userid,
|
|
326
|
+
transport_url: response.data.urls.transport_url,
|
|
327
|
+
weather_url: response.data.urls.weather_url,
|
|
328
|
+
timer: null,
|
|
329
|
+
protobufRoot: null,
|
|
330
|
+
token: googleToken,
|
|
331
|
+
cameraAPI: {
|
|
332
|
+
key: 'Authorization',
|
|
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
|
+
});
|
|
348
|
+
});
|
|
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
|
+
}
|
|
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
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async #subscribeREST(connectionType, fullRefresh) {
|
|
458
|
+
const REQUIREDBUCKETS = [
|
|
459
|
+
'buckets',
|
|
460
|
+
'structure',
|
|
461
|
+
'where',
|
|
462
|
+
'safety',
|
|
463
|
+
'device',
|
|
464
|
+
'shared',
|
|
465
|
+
'track',
|
|
466
|
+
'link',
|
|
467
|
+
'rcs_settings',
|
|
468
|
+
'schedule',
|
|
469
|
+
'kryptonite',
|
|
470
|
+
'topaz',
|
|
471
|
+
'widget_track',
|
|
472
|
+
'quartz',
|
|
473
|
+
];
|
|
474
|
+
const DEVICEBUCKETS = {
|
|
475
|
+
structure: ['latitude', 'longitude'],
|
|
476
|
+
device: ['where_id'],
|
|
477
|
+
kryptonite: ['where_id', 'structure_id'],
|
|
478
|
+
topaz: ['where_id', 'structure_id'],
|
|
479
|
+
quartz: ['where_id', 'structure_id', 'nexus_api_http_server_url'],
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
let restAPIURL = '';
|
|
483
|
+
let restAPIJSONData = {};
|
|
484
|
+
if (Object.keys(this.#rawData).length === 0 || (typeof fullRefresh === 'boolean' && fullRefresh === true)) {
|
|
485
|
+
// Setup for a full data read from REST API
|
|
486
|
+
restAPIURL =
|
|
487
|
+
'https://' +
|
|
488
|
+
this.#connections[connectionType].restAPIHost +
|
|
489
|
+
'/api/0.1/user/' +
|
|
490
|
+
this.#connections[connectionType].userID +
|
|
491
|
+
'/app_launch';
|
|
492
|
+
restAPIJSONData = { known_bucket_types: REQUIREDBUCKETS, known_bucket_versions: [] };
|
|
493
|
+
}
|
|
494
|
+
if (Object.keys(this.#rawData).length !== 0 && typeof fullRefresh === 'boolean' && fullRefresh === false) {
|
|
495
|
+
// Setup to subscribe to object changes we know about from REST API
|
|
496
|
+
restAPIURL = this.#connections[connectionType].transport_url + '/v6/subscribe';
|
|
497
|
+
restAPIJSONData = { objects: [] };
|
|
498
|
+
|
|
499
|
+
Object.entries(this.#rawData).forEach(([object_key]) => {
|
|
500
|
+
if (
|
|
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({
|
|
507
|
+
object_key: object_key,
|
|
508
|
+
object_revision: this.#rawData[object_key].object_revision,
|
|
509
|
+
object_timestamp: this.#rawData[object_key].object_timestamp,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let request = {
|
|
516
|
+
method: 'post',
|
|
517
|
+
url: restAPIURL,
|
|
518
|
+
responseType: 'json',
|
|
519
|
+
headers: {
|
|
520
|
+
'User-Agent': USERAGENT,
|
|
521
|
+
Authorization: 'Basic ' + this.#connections[connectionType].token,
|
|
522
|
+
},
|
|
523
|
+
data: JSON.stringify(restAPIJSONData),
|
|
524
|
+
};
|
|
525
|
+
axios(request)
|
|
526
|
+
.then(async (response) => {
|
|
527
|
+
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
528
|
+
throw new Error('REST API subscription failed with error');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
let data = {};
|
|
532
|
+
let deviceChanges = []; // No REST API devices changes to start with
|
|
533
|
+
if (typeof response.data?.updated_buckets === 'object') {
|
|
534
|
+
// This response is full data read
|
|
535
|
+
data = response.data.updated_buckets;
|
|
536
|
+
}
|
|
537
|
+
if (typeof response.data?.objects === 'object') {
|
|
538
|
+
// This response contains subscribed data updates
|
|
539
|
+
data = response.data.objects;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Process the data we received
|
|
543
|
+
fullRefresh = false; // Not a full data refresh required when we start again
|
|
544
|
+
await Promise.all(
|
|
545
|
+
data.map(async (value) => {
|
|
546
|
+
if (value.object_key.startsWith('structure.') === true) {
|
|
547
|
+
// Since we have a structure key, need to add in weather data for the location using latitude and longitude details
|
|
548
|
+
if (typeof value.value?.weather !== 'object') {
|
|
549
|
+
value.value.weather = {};
|
|
550
|
+
}
|
|
551
|
+
if (
|
|
552
|
+
typeof this.#rawData[value.object_key] === 'object' &&
|
|
553
|
+
typeof this.#rawData[value.object_key].value?.weather === 'object'
|
|
554
|
+
) {
|
|
555
|
+
value.value.weather = this.#rawData[value.object_key].value.weather;
|
|
556
|
+
}
|
|
557
|
+
value.value.weather = await this.#getWeatherData(
|
|
558
|
+
connectionType,
|
|
559
|
+
value.object_key,
|
|
560
|
+
value.value.latitude,
|
|
561
|
+
value.value.longitude,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// Check for changes in the swarm property. This seems indicate changes in devices
|
|
565
|
+
if (typeof this.#rawData[value.object_key] === 'object') {
|
|
566
|
+
this.#rawData[value.object_key].value.swarm.map((object_key) => {
|
|
567
|
+
if (value.value.swarm.includes(object_key) === false) {
|
|
568
|
+
// Object is present in the old swarm list, but not in the new swarm list, so we assume it has been removed
|
|
569
|
+
// We'll remove the associated object here for future subscribe
|
|
570
|
+
delete this.#rawData[object_key];
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (value.object_key.startsWith('quartz.') === true) {
|
|
577
|
+
// We have camera(s) and/or doorbell(s), so get extra details that are required
|
|
578
|
+
value.value.properties =
|
|
579
|
+
typeof this.#rawData[value.object_key]?.value?.properties === 'object'
|
|
580
|
+
? this.#rawData[value.object_key].value.properties
|
|
581
|
+
: [];
|
|
582
|
+
|
|
583
|
+
let request = {
|
|
584
|
+
method: 'get',
|
|
585
|
+
url:
|
|
586
|
+
'https://webapi.' +
|
|
587
|
+
this.#connections[connectionType].cameraAPIHost +
|
|
588
|
+
'/api/cameras.get_with_properties?uuid=' +
|
|
589
|
+
value.object_key.split('.')[1],
|
|
590
|
+
headers: {
|
|
591
|
+
referer: 'https://' + this.#connections[connectionType].referer,
|
|
592
|
+
'User-Agent': USERAGENT,
|
|
593
|
+
[this.#connections[connectionType].cameraAPI.key]:
|
|
594
|
+
this.#connections[connectionType].cameraAPI.value + this.#connections[connectionType].cameraAPI.token,
|
|
595
|
+
},
|
|
596
|
+
responseType: 'json',
|
|
597
|
+
timeout: NESTAPITIMEOUT,
|
|
598
|
+
};
|
|
599
|
+
await axios(request)
|
|
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;
|
|
606
|
+
})
|
|
607
|
+
.catch((error) => {
|
|
608
|
+
this?.log?.debug &&
|
|
609
|
+
this?.log?.debug &&
|
|
610
|
+
this.log.debug('REST API had error retrieving camera/doorbell details. Error was "%s"', error?.code);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
value.value.activity_zones =
|
|
614
|
+
typeof this.#rawData[value.object_key]?.value?.activity_zones === 'object'
|
|
615
|
+
? this.#rawData[value.object_key].value.activity_zones
|
|
616
|
+
: [];
|
|
617
|
+
|
|
618
|
+
request = {
|
|
619
|
+
method: 'get',
|
|
620
|
+
url: value.value.nexus_api_http_server_url + '/cuepoint_category/' + value.object_key.split('.')[1],
|
|
621
|
+
headers: {
|
|
622
|
+
referer: 'https://' + this.#connections[connectionType].referer,
|
|
623
|
+
'User-Agent': USERAGENT,
|
|
624
|
+
[this.#connections[connectionType].cameraAPI.key]:
|
|
625
|
+
this.#connections[connectionType].cameraAPI.value + this.#connections[connectionType].cameraAPI.token,
|
|
626
|
+
},
|
|
627
|
+
responseType: 'json',
|
|
628
|
+
timeout: NESTAPITIMEOUT,
|
|
629
|
+
};
|
|
630
|
+
await axios(request)
|
|
631
|
+
.then((response) => {
|
|
632
|
+
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
633
|
+
throw new Error('REST API had error retrieving camera/doorbell activity zones');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
let zones = [];
|
|
637
|
+
response.data.forEach((zone) => {
|
|
638
|
+
if (zone.type.toUpperCase() === 'ACTIVITY' || zone.type.toUpperCase() === 'REGION') {
|
|
639
|
+
zones.push({
|
|
640
|
+
id: zone.id === 0 ? 1 : zone.id,
|
|
641
|
+
name: makeHomeKitName(zone.label),
|
|
642
|
+
hidden: zone.hidden === true,
|
|
643
|
+
uri: zone.nexusapi_image_uri,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
value.value.activity_zones = zones;
|
|
649
|
+
})
|
|
650
|
+
.catch((error) => {
|
|
651
|
+
this?.log?.debug &&
|
|
652
|
+
this?.log?.debug &&
|
|
653
|
+
this.log.debug('REST API had error retrieving camera/doorbell activity zones. Error was "%s"', error?.code);
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (value.object_key.startsWith('buckets.') === true) {
|
|
658
|
+
if (
|
|
659
|
+
typeof this.#rawData[value.object_key] === 'object' &&
|
|
660
|
+
typeof this.#rawData[value.object_key].value?.buckets === 'object'
|
|
661
|
+
) {
|
|
662
|
+
// Check for added objects
|
|
663
|
+
value.value.buckets.map((object_key) => {
|
|
664
|
+
if (this.#rawData[value.object_key].value.buckets.includes(object_key) === false) {
|
|
665
|
+
// Since this is an added object to the raw REST API structure, we need to do a full read of the data
|
|
666
|
+
fullRefresh = true;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Check for removed objects
|
|
671
|
+
this.#rawData[value.object_key].value.buckets.map((object_key) => {
|
|
672
|
+
if (value.value.buckets.includes(object_key) === false) {
|
|
673
|
+
// Object is present in the old buckets list, but not in the new buckets list
|
|
674
|
+
// so we assume it has been removed
|
|
675
|
+
// It also could mean device(s) have been removed from Nest
|
|
676
|
+
if (Object.keys(DEVICEBUCKETS).includes(object_key.split('.')[0]) === true) {
|
|
677
|
+
deviceChanges.push({ object_key: object_key, change: 'remove' });
|
|
678
|
+
}
|
|
679
|
+
delete this.#rawData[object_key];
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Store or update the date in our internally saved raw REST API data
|
|
686
|
+
if (typeof this.#rawData[value.object_key] === 'undefined') {
|
|
687
|
+
this.#rawData[value.object_key] = {};
|
|
688
|
+
this.#rawData[value.object_key].object_revision = value.object_revision;
|
|
689
|
+
this.#rawData[value.object_key].object_timestamp = value.object_timestamp;
|
|
690
|
+
this.#rawData[value.object_key].connection = connectionType;
|
|
691
|
+
this.#rawData[value.object_key].source = NestAccfactory.DataSource.REST;
|
|
692
|
+
this.#rawData[value.object_key].timers = {}; // No timers running for this object
|
|
693
|
+
this.#rawData[value.object_key].value = {};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Need to check for a possible device addition to the raw REST API data.
|
|
697
|
+
// We expect the devices we want to add, have certain minimum properties present in the data
|
|
698
|
+
// We'll perform that check here
|
|
699
|
+
if (
|
|
700
|
+
Object.keys(DEVICEBUCKETS).includes(value.object_key.split('.')[0]) === true &&
|
|
701
|
+
DEVICEBUCKETS[value.object_key.split('.')[0]].every((key) => key in value.value) === true &&
|
|
702
|
+
DEVICEBUCKETS[value.object_key.split('.')[0]].every((key) => key in this.#rawData[value.object_key].value) === false
|
|
703
|
+
) {
|
|
704
|
+
deviceChanges.push({ object_key: value.object_key, change: 'add' });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Finally, update our internal raw REST API data with the new values
|
|
708
|
+
this.#rawData[value.object_key].object_revision = value.object_revision; // Used for future subscribes
|
|
709
|
+
this.#rawData[value.object_key].object_timestamp = value.object_timestamp; // Used for future subscribes
|
|
710
|
+
for (const [fieldKey, fieldValue] of Object.entries(value.value)) {
|
|
711
|
+
this.#rawData[value.object_key]['value'][fieldKey] = fieldValue;
|
|
712
|
+
}
|
|
713
|
+
}),
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
await this.#processPostSubscribe(deviceChanges);
|
|
717
|
+
})
|
|
718
|
+
.catch((error) => {
|
|
719
|
+
if (error?.code !== 'ECONNRESET') {
|
|
720
|
+
this?.log?.error && this.log.error('REST API subscription failed with error "%s"', error?.code);
|
|
721
|
+
}
|
|
722
|
+
})
|
|
723
|
+
.finally(() => {
|
|
724
|
+
setTimeout(this.#subscribeREST.bind(this, connectionType, fullRefresh), 1000);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async #subscribeProtobuf(connectionType) {
|
|
729
|
+
const calculate_message_size = (inputBuffer) => {
|
|
730
|
+
// First byte in the is a tag type??
|
|
731
|
+
// Following is a varint type
|
|
732
|
+
// After varint size, is the buffer content
|
|
733
|
+
let varint = 0;
|
|
734
|
+
let bufferPos = 0;
|
|
735
|
+
let currentByte;
|
|
736
|
+
|
|
737
|
+
for (;;) {
|
|
738
|
+
currentByte = inputBuffer[bufferPos + 1]; // Offset in buffer + 1 to skip over starting tag
|
|
739
|
+
varint |= (currentByte & 0x7f) << (bufferPos * 7);
|
|
740
|
+
bufferPos += 1;
|
|
741
|
+
if (bufferPos > 5) {
|
|
742
|
+
throw new Error('VarInt exceeds allowed bounds.');
|
|
743
|
+
}
|
|
744
|
+
if ((currentByte & 0x80) !== 0x80) {
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Return length of message in buffer
|
|
750
|
+
return varint + bufferPos + 1;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const traverseTypes = (currentTrait, callback) => {
|
|
754
|
+
if (currentTrait instanceof protobuf.Type) {
|
|
755
|
+
callback(currentTrait);
|
|
756
|
+
}
|
|
757
|
+
if (currentTrait.nestedArray) {
|
|
758
|
+
currentTrait.nestedArray.map((trait) => {
|
|
759
|
+
traverseTypes(trait, callback);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
let observeTraits = null;
|
|
765
|
+
if (fs.existsSync(path.resolve(__dirname + '/protobuf/root.proto')) === true) {
|
|
766
|
+
protobuf.util.Long = null;
|
|
767
|
+
protobuf.configure();
|
|
768
|
+
this.#connections[connectionType].protobufRoot = protobuf.loadSync(path.resolve(__dirname + '/protobuf/root.proto'));
|
|
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
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
let request = {
|
|
797
|
+
method: 'post',
|
|
798
|
+
url: 'https://' + this.#connections[connectionType].protobufAPIHost + '/nestlabs.gateway.v2.GatewayService/Observe',
|
|
799
|
+
headers: {
|
|
800
|
+
'User-Agent': USERAGENT,
|
|
801
|
+
Authorization: 'Basic ' + this.#connections[connectionType].token,
|
|
802
|
+
'Content-Type': 'application/x-protobuf',
|
|
803
|
+
'X-Accept-Content-Transfer-Encoding': 'binary',
|
|
804
|
+
'X-Accept-Response-Streaming': 'true',
|
|
805
|
+
},
|
|
806
|
+
responseType: 'stream',
|
|
807
|
+
data: observeTraits,
|
|
808
|
+
};
|
|
809
|
+
axios(request)
|
|
810
|
+
.then(async (response) => {
|
|
811
|
+
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
812
|
+
throw new Error('protobuf API had error perform trait observe');
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
let deviceChanges = []; // No protobuf API devices changes to start with
|
|
816
|
+
let buffer = Buffer.alloc(0);
|
|
817
|
+
for await (const chunk of response.data) {
|
|
818
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
819
|
+
let messageSize = calculate_message_size(buffer);
|
|
820
|
+
if (buffer.length >= messageSize) {
|
|
821
|
+
let decodedMessage = {};
|
|
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
|
+
}
|
|
837
|
+
|
|
838
|
+
// Tidy up our received messages. This ensures we only have one status for the trait in the data we process
|
|
839
|
+
// We'll favour a trait with accepted status over the same with confirmed status
|
|
840
|
+
let notAcceptedStatus = decodedMessage.message[0].get.filter((trait) => trait.stateTypes.includes('ACCEPTED') === false);
|
|
841
|
+
let acceptedStatus = decodedMessage.message[0].get.filter((trait) => trait.stateTypes.includes('ACCEPTED') === true);
|
|
842
|
+
let difference = acceptedStatus.map((trait) => trait.traitId.resourceId + '/' + trait.traitId.traitLabel);
|
|
843
|
+
decodedMessage.message[0].get =
|
|
844
|
+
((notAcceptedStatus = notAcceptedStatus.filter(
|
|
845
|
+
(trait) => difference.includes(trait.traitId.resourceId + '/' + trait.traitId.traitLabel) === false,
|
|
846
|
+
)),
|
|
847
|
+
[...notAcceptedStatus, ...acceptedStatus]);
|
|
848
|
+
|
|
849
|
+
// We'll use the resource status message to look for structure and/or device removals
|
|
850
|
+
// We could also check for structure and/or device additions here, but we'll want to be flagged
|
|
851
|
+
// that a device is 'ready' for use before we add in. This data is populated in the trait data
|
|
852
|
+
decodedMessage.message[0].resourceMetas.map(async (resource) => {
|
|
853
|
+
if (
|
|
854
|
+
resource.status === 'REMOVED' &&
|
|
855
|
+
(resource.resourceId.startsWith('STRUCTURE_') || resource.resourceId.startsWith('DEVICE_'))
|
|
856
|
+
) {
|
|
857
|
+
// We have the removal of a 'home' and/ device
|
|
858
|
+
deviceChanges.push({ object_key: resource.resourceId, change: 'removed' });
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
// eslint-disable-next-line no-unused-vars
|
|
862
|
+
} catch (error) {
|
|
863
|
+
// Empty
|
|
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') {
|
|
871
|
+
if (
|
|
872
|
+
this.#rawData[trait.traitId.resourceId]?.value?.configuration_done?.deviceReady !== true &&
|
|
873
|
+
trait.patch.values?.deviceReady === true
|
|
874
|
+
) {
|
|
875
|
+
deviceChanges.push({ object_key: trait.traitId.resourceId, change: 'add' });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (trait.traitId.traitLabel === 'camera_migration_status') {
|
|
879
|
+
// Handle case of camera/doorbell(s) which have been migrated from Nest to Google Home
|
|
880
|
+
if (
|
|
881
|
+
this.#rawData[trait.traitId.resourceId]?.value?.camera_migration_status?.state?.where !== 'MIGRATED_TO_GOOGLE_HOME' &&
|
|
882
|
+
trait.patch.values?.state?.where === 'MIGRATED_TO_GOOGLE_HOME' &&
|
|
883
|
+
this.#rawData[trait.traitId.resourceId]?.value?.camera_migration_status?.state?.progress !== 'PROGRESS_COMPLETE' &&
|
|
884
|
+
trait.patch.values?.state?.progress === 'PROGRESS_COMPLETE'
|
|
885
|
+
) {
|
|
886
|
+
deviceChanges.push({ object_key: trait.traitId.resourceId, change: 'add' });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (typeof this.#rawData[trait.traitId.resourceId] === 'undefined') {
|
|
891
|
+
this.#rawData[trait.traitId.resourceId] = {};
|
|
892
|
+
this.#rawData[trait.traitId.resourceId].connection = connectionType;
|
|
893
|
+
this.#rawData[trait.traitId.resourceId].source = NestAccfactory.DataSource.PROTOBUF;
|
|
894
|
+
this.#rawData[trait.traitId.resourceId].timers = {}; // No timers running for this object
|
|
895
|
+
this.#rawData[trait.traitId.resourceId].value = {};
|
|
896
|
+
}
|
|
897
|
+
this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel] =
|
|
898
|
+
typeof trait.patch.values !== 'undefined' ? trait.patch.values : {};
|
|
899
|
+
|
|
900
|
+
// We don't need to store the trait type, so remove it
|
|
901
|
+
delete this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel]['@type'];
|
|
902
|
+
|
|
903
|
+
// If we have structure location details and associated geo-location details, get the weather data for the location
|
|
904
|
+
// We'll store this in the object key/value as per REST API
|
|
905
|
+
if (
|
|
906
|
+
trait.traitId.resourceId.startsWith('STRUCTURE_') === true &&
|
|
907
|
+
trait.traitId.traitLabel === 'structure_location' &&
|
|
908
|
+
typeof trait.patch.values?.geoCoordinate?.latitude === 'number' &&
|
|
909
|
+
typeof trait.patch.values?.geoCoordinate?.longitude === 'number'
|
|
910
|
+
) {
|
|
911
|
+
this.#rawData[trait.traitId.resourceId].value.weather = await this.#getWeatherData(
|
|
912
|
+
connectionType,
|
|
913
|
+
trait.traitId.resourceId,
|
|
914
|
+
trait.patch.values.geoCoordinate.latitude,
|
|
915
|
+
trait.patch.values.geoCoordinate.longitude,
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
}),
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
await this.#processPostSubscribe(deviceChanges);
|
|
922
|
+
deviceChanges = []; // No more device changes now
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
})
|
|
927
|
+
.catch((error) => {
|
|
928
|
+
if (error?.code !== 'ECONNRESET') {
|
|
929
|
+
this?.log?.error && this.log.error('protobuf API had error perform trait observe. Error was "%s"', error?.code);
|
|
930
|
+
}
|
|
931
|
+
})
|
|
932
|
+
.finally(() => {
|
|
933
|
+
setTimeout(this.#subscribeProtobuf.bind(this, connectionType), 1000);
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async #processPostSubscribe(deviceChanges) {
|
|
938
|
+
// Process any device removals we have
|
|
939
|
+
Object.values(deviceChanges)
|
|
940
|
+
.filter((object) => object.change === 'remove')
|
|
941
|
+
.forEach((object) => {
|
|
942
|
+
if (typeof this.#rawData[object.object_key] === 'object') {
|
|
943
|
+
// Remove any timers that might have been associated with this device
|
|
944
|
+
Object.values(this.#rawData[object.object_key].timers).forEach((timerObject) => {
|
|
945
|
+
clearInterval(timerObject);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// Clean up structure
|
|
949
|
+
delete this.#rawData[object.object_key];
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Send removed notice onto HomeKit device for it to process
|
|
953
|
+
// This allows handling removal of a device without knowing its previous data
|
|
954
|
+
this.#eventEmitter.emit(object.object_key, HomeKitDevice.REMOVE, {});
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
Object.values(this.#processData('')).forEach((deviceData) => {
|
|
958
|
+
// Process any device additions we have
|
|
959
|
+
Object.values(deviceChanges)
|
|
960
|
+
.filter((object) => object.change === 'add')
|
|
961
|
+
.forEach((object) => {
|
|
962
|
+
if (object.object_key === deviceData.uuid && deviceData.excluded === true) {
|
|
963
|
+
this?.log?.warn && this.log.warn('Device "%s" ignored due to it being marked as excluded', deviceData.description);
|
|
964
|
+
}
|
|
965
|
+
if (object.object_key === deviceData.uuid && deviceData.excluded === false) {
|
|
966
|
+
// Device isn't marked as excluded, so create the required HomeKit accessories based upon the device data
|
|
967
|
+
if (deviceData.device_type === NestAccfactory.DeviceType.THERMOSTAT && typeof NestThermostat === 'function') {
|
|
968
|
+
// Nest Thermostat(s) - Categories.THERMOSTAT = 9
|
|
969
|
+
let tempDevice = new NestThermostat(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
970
|
+
tempDevice.add('Nest Thermostat', 9, true);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (deviceData.device_type === NestAccfactory.DeviceType.TEMPSENSOR && typeof NestTemperatureSensor === 'function') {
|
|
974
|
+
// Nest Temperature Sensor - Categories.SENSOR = 10
|
|
975
|
+
let tempDevice = new NestTemperatureSensor(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
976
|
+
tempDevice.add('Nest Temperature Sensor', 10, true);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (deviceData.device_type === NestAccfactory.DeviceType.SMOKESENSOR && typeof NestProtect === 'function') {
|
|
980
|
+
// Nest Protect(s) - Categories.SENSOR = 10
|
|
981
|
+
let tempDevice = new NestProtect(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
982
|
+
tempDevice.add('Nest Protect', 10, true);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (
|
|
986
|
+
(deviceData.device_type === NestAccfactory.DeviceType.CAMERA ||
|
|
987
|
+
deviceData.device_type === NestAccfactory.DeviceType.DOORBELL) &&
|
|
988
|
+
(typeof NestCamera === 'function' || typeof NestDoorbell === 'function')
|
|
989
|
+
) {
|
|
990
|
+
let accessoryName = 'Nest ' + deviceData.model.replace(/\s*(?:\([^()]*\))/gi, '');
|
|
991
|
+
if (deviceData.device_type === NestAccfactory.DeviceType.CAMERA) {
|
|
992
|
+
// Nest Camera(s) - Categories.IP_CAMERA = 17
|
|
993
|
+
let tempDevice = new NestCamera(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
994
|
+
tempDevice.add(accessoryName, 17, true);
|
|
995
|
+
}
|
|
996
|
+
if (deviceData.device_type === NestAccfactory.DeviceType.DOORBELL) {
|
|
997
|
+
// Nest Doorbell(s) - Categories.VIDEO_DOORBELL = 18
|
|
998
|
+
let tempDevice = new NestDoorbell(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
999
|
+
tempDevice.add(accessoryName, 18, true);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// 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 protobuf API
|
|
1004
|
+
if (
|
|
1005
|
+
typeof this.#rawData[object.object_key]?.timers?.zones === 'undefined' &&
|
|
1006
|
+
this.#rawData[object.object_key].source === NestAccfactory.DataSource.REST
|
|
1007
|
+
) {
|
|
1008
|
+
this.#rawData[object.object_key].timers.zones = setInterval(async () => {
|
|
1009
|
+
if (typeof this.#rawData[object.object_key]?.value === 'object') {
|
|
1010
|
+
let request = {
|
|
1011
|
+
method: 'get',
|
|
1012
|
+
url:
|
|
1013
|
+
this.#rawData[object.object_key].value.nexus_api_http_server_url +
|
|
1014
|
+
'/cuepoint_category/' +
|
|
1015
|
+
object.object_key.split('.')[1],
|
|
1016
|
+
headers: {
|
|
1017
|
+
referer: 'https://' + this.#connections[this.#rawData[object.object_key].connection].referer,
|
|
1018
|
+
'User-Agent': USERAGENT,
|
|
1019
|
+
[this.#connections[this.#rawData[object.object_key].connection].cameraAPI.key]:
|
|
1020
|
+
this.#connections[this.#rawData[object.object_key].connection].cameraAPI.value +
|
|
1021
|
+
this.#connections[this.#rawData[object.object_key].connection].cameraAPI.token,
|
|
1022
|
+
},
|
|
1023
|
+
responseType: 'json',
|
|
1024
|
+
timeout: CAMERAZONEPOLLING,
|
|
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
|
+
|
|
1032
|
+
let zones = [];
|
|
1033
|
+
response.data.forEach((zone) => {
|
|
1034
|
+
if (zone.type.toUpperCase() === 'ACTIVITY' || zone.type.toUpperCase() === 'REGION') {
|
|
1035
|
+
zones.push({
|
|
1036
|
+
id: zone.id === 0 ? 1 : zone.id,
|
|
1037
|
+
name: makeHomeKitName(zone.label),
|
|
1038
|
+
hidden: zone.hidden === true,
|
|
1039
|
+
uri: zone.nexusapi_image_uri,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
this.#rawData[object.object_key].value.activity_zones = zones;
|
|
1045
|
+
|
|
1046
|
+
// Send updated data onto HomeKit device for it to process
|
|
1047
|
+
this.#eventEmitter.emit(object.object_key, HomeKitDevice.UPDATE, {
|
|
1048
|
+
activity_zones: this.#rawData[object.object_key].value.activity_zones,
|
|
1049
|
+
});
|
|
1050
|
+
})
|
|
1051
|
+
.catch((error) => {
|
|
1052
|
+
// Log debug message if wasn't a timeout
|
|
1053
|
+
if (error?.code !== 'ECONNABORTED') {
|
|
1054
|
+
this?.log?.debug &&
|
|
1055
|
+
this.log.debug(
|
|
1056
|
+
'REST API had error retrieving camera/doorbell activity zones for uuid "%s". Error was "%s"',
|
|
1057
|
+
object.object_key,
|
|
1058
|
+
error?.code,
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}, CAMERAZONEPOLLING);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Setup polling loop for camera/doorbell alert data if not already created
|
|
1067
|
+
if (typeof this.#rawData[object.object_key]?.timers?.alerts === 'undefined') {
|
|
1068
|
+
this.#rawData[object.object_key].timers.alerts = setInterval(async () => {
|
|
1069
|
+
if (
|
|
1070
|
+
typeof this.#rawData[object.object_key]?.value === 'object' &&
|
|
1071
|
+
this.#rawData[object.object_key]?.source === NestAccfactory.DataSource.PROTOBUF
|
|
1072
|
+
) {
|
|
1073
|
+
let alerts = []; // No alerts yet
|
|
1074
|
+
|
|
1075
|
+
let commandResponse = await this.#protobufCommand(object.object_key, [
|
|
1076
|
+
{
|
|
1077
|
+
traitLabel: 'camera_observation_history',
|
|
1078
|
+
command: {
|
|
1079
|
+
type_url: 'type.nestlabs.com/nest.trait.history.CameraObservationHistoryTrait.CameraObservationHistoryRequest',
|
|
1080
|
+
value: {
|
|
1081
|
+
// We want camera history from now for upto 30secs from now
|
|
1082
|
+
queryStartTime: { seconds: Math.floor(Date.now() / 1000), nanos: (Math.round(Date.now()) % 1000) * 1e6 },
|
|
1083
|
+
queryEndTime: {
|
|
1084
|
+
seconds: Math.floor((Date.now() + 30000) / 1000),
|
|
1085
|
+
nanos: (Math.round(Date.now() + 30000) % 1000) * 1e6,
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
]);
|
|
1091
|
+
|
|
1092
|
+
if (
|
|
1093
|
+
typeof commandResponse?.resourceCommandResponse?.[0]?.traitOperations?.[0]?.event?.event?.cameraEventWindow
|
|
1094
|
+
?.cameraEvent === 'object'
|
|
1095
|
+
) {
|
|
1096
|
+
commandResponse.resourceCommandResponse[0].traitOperations[0].event.event.cameraEventWindow.cameraEvent.forEach(
|
|
1097
|
+
(event) => {
|
|
1098
|
+
alerts.push({
|
|
1099
|
+
playback_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
|
|
1100
|
+
start_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
|
|
1101
|
+
end_time: parseInt(event.endTime.seconds) * 1000 + parseInt(event.endTime.nanos) / 1000000,
|
|
1102
|
+
id: event.eventId,
|
|
1103
|
+
zone_ids:
|
|
1104
|
+
typeof event.activityZone === 'object'
|
|
1105
|
+
? event.activityZone.map((zone) =>
|
|
1106
|
+
typeof zone?.zoneIndex === 'number' ? zone.zoneIndex : zone.internalIndex,
|
|
1107
|
+
)
|
|
1108
|
+
: [],
|
|
1109
|
+
types: event.eventType
|
|
1110
|
+
.map((event) => (event.startsWith('EVENT_') === true ? event.split('EVENT_')[1].toLowerCase() : ''))
|
|
1111
|
+
.filter((event) => event),
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Fix up event types to match REST API
|
|
1115
|
+
// 'EVENT_UNFAMILIAR_FACE' = 'unfamiliar-face'
|
|
1116
|
+
// 'EVENT_PERSON_TALKING' = 'personHeard'
|
|
1117
|
+
// 'EVENT_DOG_BARKING' = 'dogBarking'
|
|
1118
|
+
// <---- TODO (as the ones we use match from protobuf)
|
|
1119
|
+
},
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
// Sort alerts to be most recent first
|
|
1123
|
+
alerts = alerts.sort((a, b) => {
|
|
1124
|
+
if (a.start_time > b.start_time) {
|
|
1125
|
+
return -1;
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
this.#rawData[object.object_key].value.alerts = alerts;
|
|
1131
|
+
|
|
1132
|
+
// Send updated data onto HomeKit device for it to process
|
|
1133
|
+
this.#eventEmitter.emit(object.object_key, HomeKitDevice.UPDATE, {
|
|
1134
|
+
alerts: this.#rawData[object.object_key].value.alerts,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (
|
|
1139
|
+
typeof this.#rawData[object.object_key]?.value === 'object' &&
|
|
1140
|
+
this.#rawData[object.object_key]?.source === NestAccfactory.DataSource.REST
|
|
1141
|
+
) {
|
|
1142
|
+
let alerts = []; // No alerts yet
|
|
1143
|
+
let request = {
|
|
1144
|
+
method: 'get',
|
|
1145
|
+
url:
|
|
1146
|
+
this.#rawData[object.object_key].value.nexus_api_http_server_url +
|
|
1147
|
+
'/cuepoint/' +
|
|
1148
|
+
object.object_key.split('.')[1] +
|
|
1149
|
+
'/2?start_time=' +
|
|
1150
|
+
Math.floor(Date.now() / 1000 - 30),
|
|
1151
|
+
headers: {
|
|
1152
|
+
referer: 'https://' + this.#connections[this.#rawData[object.object_key].connection].referer,
|
|
1153
|
+
'User-Agent': USERAGENT,
|
|
1154
|
+
[this.#connections[this.#rawData[object.object_key].connection].cameraAPI.key]:
|
|
1155
|
+
this.#connections[this.#rawData[object.object_key].connection].cameraAPI.value +
|
|
1156
|
+
this.#connections[this.#rawData[object.object_key].connection].cameraAPI.token,
|
|
1157
|
+
},
|
|
1158
|
+
responseType: 'json',
|
|
1159
|
+
timeout: CAMERAALERTPOLLING,
|
|
1160
|
+
};
|
|
1161
|
+
await axios(request)
|
|
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) => {
|
|
1168
|
+
// Fix up alert zone IDs. If there is an ID of 0, we'll transform to 1. ie: main zone
|
|
1169
|
+
// If there are NO zone IDs, we'll put a 1 in there ie: main zone
|
|
1170
|
+
alert.zone_ids = alert.zone_ids.map((id) => (id !== 0 ? id : 1));
|
|
1171
|
+
if (alert.zone_ids.length === 0) {
|
|
1172
|
+
alert.zone_ids.push(1);
|
|
1173
|
+
}
|
|
1174
|
+
alerts.push({
|
|
1175
|
+
playback_time: alert.playback_time,
|
|
1176
|
+
start_time: alert.start_time,
|
|
1177
|
+
end_time: alert.end_time,
|
|
1178
|
+
id: alert.id,
|
|
1179
|
+
zone_ids: alert.zone_ids,
|
|
1180
|
+
types: alert.types,
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
// Sort alerts to be most recent first
|
|
1185
|
+
alerts = alerts.sort((a, b) => {
|
|
1186
|
+
if (a.start_time > b.start_time) {
|
|
1187
|
+
return -1;
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
})
|
|
1191
|
+
.catch((error) => {
|
|
1192
|
+
// Log debug message if wasn't a timeout
|
|
1193
|
+
if (error?.code !== 'ECONNABORTED') {
|
|
1194
|
+
this?.log?.debug &&
|
|
1195
|
+
this.log.debug(
|
|
1196
|
+
'REST API had error retrieving camera/doorbell activity notifications for uuid "%s". Error was "%s"',
|
|
1197
|
+
object.object_key,
|
|
1198
|
+
error?.code,
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
this.#rawData[object.object_key].value.alerts = alerts;
|
|
1204
|
+
|
|
1205
|
+
// Send updated data onto HomeKit device for it to process
|
|
1206
|
+
this.#eventEmitter.emit(object.object_key, HomeKitDevice.UPDATE, {
|
|
1207
|
+
alerts: this.#rawData[object.object_key].value.alerts,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}, CAMERAALERTPOLLING);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
if (deviceData.device_type === NestAccfactory.DeviceType.WEATHER && typeof NestWeather === 'function') {
|
|
1214
|
+
// Nest 'Virtual' weather station - Categories.SENSOR = 10
|
|
1215
|
+
let tempDevice = new NestWeather(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
1216
|
+
tempDevice.add('Nest Weather', 10, true);
|
|
1217
|
+
|
|
1218
|
+
// Setup polling loop for weather data if not already created
|
|
1219
|
+
if (typeof this.#rawData[object.object_key]?.timers?.weather === 'undefined') {
|
|
1220
|
+
this.#rawData[object.object_key].timers.weather = setInterval(async () => {
|
|
1221
|
+
this.#rawData[object.object_key].value.weather = await this.#getWeatherData(
|
|
1222
|
+
this.#rawData[object.object_key].connection,
|
|
1223
|
+
object.object_key,
|
|
1224
|
+
this.#rawData[object.object_key].value.weather.latitude,
|
|
1225
|
+
this.#rawData[object.object_key].value.weather.longitude,
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
// Send updated data onto HomeKit device for it to process
|
|
1229
|
+
this.#eventEmitter.emit(
|
|
1230
|
+
object.object_key,
|
|
1231
|
+
HomeKitDevice.UPDATE,
|
|
1232
|
+
this.#processData(object.object_key)[deviceData.serial_number],
|
|
1233
|
+
);
|
|
1234
|
+
}, WEATHERPOLLING);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
// Finally, after processing device additions, if device is not excluded, send updated data to device for it to process
|
|
1241
|
+
if (deviceData.excluded === false) {
|
|
1242
|
+
this.#eventEmitter.emit(deviceData.uuid, HomeKitDevice.UPDATE, deviceData);
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
#processData(deviceUUID) {
|
|
1248
|
+
if (typeof deviceUUID !== 'string') {
|
|
1249
|
+
deviceUUID = '';
|
|
1250
|
+
}
|
|
1251
|
+
let devices = {};
|
|
1252
|
+
|
|
1253
|
+
// Get the device(s) location from stucture
|
|
1254
|
+
// We'll test in both REST and protobuf API data
|
|
1255
|
+
const get_location_name = (structure_id, where_id) => {
|
|
1256
|
+
let location = '';
|
|
1257
|
+
|
|
1258
|
+
// Check REST data
|
|
1259
|
+
if (typeof this.#rawData['where.' + structure_id]?.value === 'object') {
|
|
1260
|
+
this.#rawData['where.' + structure_id].value.wheres.forEach((value) => {
|
|
1261
|
+
if (where_id === value.where_id) {
|
|
1262
|
+
location = value.name;
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Check protobuf data
|
|
1268
|
+
if (typeof this.#rawData[structure_id]?.value?.located_annotations?.predefinedWheres === 'object') {
|
|
1269
|
+
Object.values(this.#rawData[structure_id].value.located_annotations.predefinedWheres).forEach((value) => {
|
|
1270
|
+
if (value.whereId.resourceId === where_id) {
|
|
1271
|
+
location = value.label.literal;
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof this.#rawData[structure_id]?.value?.located_annotations?.customWheres === 'object') {
|
|
1276
|
+
Object.values(this.#rawData[structure_id].value.located_annotations.customWheres).forEach((value) => {
|
|
1277
|
+
if (value.whereId.resourceId === where_id) {
|
|
1278
|
+
location = value.label.literal;
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return location;
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
// Process data for any thermostat(s) we have in the raw data
|
|
1287
|
+
const process_thermostat_data = (object_key, data) => {
|
|
1288
|
+
let processed = {};
|
|
1289
|
+
try {
|
|
1290
|
+
// Fix up data we need to
|
|
1291
|
+
data.serial_number = data.serial_number.toUpperCase(); // ensure serial numbers are in upper case
|
|
1292
|
+
data.excluded =
|
|
1293
|
+
typeof this.config?.devices?.[data.serial_number]?.exclude === 'boolean'
|
|
1294
|
+
? this.config.devices[data.serial_number].exclude
|
|
1295
|
+
: false; // Mark device as excluded or not
|
|
1296
|
+
data.device_type = NestAccfactory.DeviceType.THERMOSTAT; // Nest Thermostat
|
|
1297
|
+
data.uuid = object_key; // Internal structure ID
|
|
1298
|
+
data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
|
|
1299
|
+
data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
|
|
1300
|
+
data.target_temperature_high = adjustTemperature(data.target_temperature_high, 'C', 'C', true);
|
|
1301
|
+
data.target_temperature_low = adjustTemperature(data.target_temperature_low, 'C', 'C', true);
|
|
1302
|
+
data.target_temperature = adjustTemperature(data.target_temperature, 'C', 'C', true);
|
|
1303
|
+
data.backplate_temperature = adjustTemperature(data.backplate_temperature, 'C', 'C', true);
|
|
1304
|
+
data.current_temperature = adjustTemperature(data.current_temperature, 'C', 'C', true);
|
|
1305
|
+
data.battery_level = scaleValue(data.battery_level, 3.6, 3.9, 0, 100);
|
|
1306
|
+
let description = typeof data?.description === 'string' ? data.description : '';
|
|
1307
|
+
let location = typeof data?.location === 'string' ? data.location : '';
|
|
1308
|
+
if (description === '') {
|
|
1309
|
+
description = location;
|
|
1310
|
+
location = '';
|
|
1311
|
+
}
|
|
1312
|
+
data.description = makeHomeKitName(location === '' ? description : description + ' - ' + location);
|
|
1313
|
+
delete data.location;
|
|
1314
|
+
|
|
1315
|
+
// Insert details for when using HAP-NodeJS library rather than Homebridge
|
|
1316
|
+
if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
|
|
1317
|
+
data.hkPairingCode = this.config.options.hkPairingCode;
|
|
1318
|
+
}
|
|
1319
|
+
if (
|
|
1320
|
+
typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
|
|
1321
|
+
this.config.devices[data.serial_number].hkPairingCode !== ''
|
|
1322
|
+
) {
|
|
1323
|
+
data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
|
|
1324
|
+
}
|
|
1325
|
+
if (data?.hkPairingCode !== undefined && data?.mac_address !== undefined) {
|
|
1326
|
+
// Create mac_address in format of xx:xx:xx:xx:xx:xx
|
|
1327
|
+
data.hkUsername = data.mac_address
|
|
1328
|
+
.toString('hex')
|
|
1329
|
+
.split(/(..)/)
|
|
1330
|
+
.filter((s) => s)
|
|
1331
|
+
.join(':')
|
|
1332
|
+
.toUpperCase();
|
|
1333
|
+
}
|
|
1334
|
+
delete data.mac_address;
|
|
1335
|
+
|
|
1336
|
+
processed = data;
|
|
1337
|
+
// eslint-disable-next-line no-unused-vars
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
// Empty
|
|
1340
|
+
}
|
|
1341
|
+
return processed;
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
const PROTOBUF_THERMOSTAT_RESOURCES = [
|
|
1345
|
+
'nest.resource.NestLearningThermostat3Resource',
|
|
1346
|
+
'nest.resource.NestAgateDisplayResource',
|
|
1347
|
+
'nest.resource.NestOnyxResource',
|
|
1348
|
+
'google.resource.GoogleZirconium1Resource',
|
|
1349
|
+
'google.resource.GoogleBismuth1Resource',
|
|
1350
|
+
];
|
|
1351
|
+
Object.entries(this.#rawData)
|
|
1352
|
+
.filter(
|
|
1353
|
+
([key, value]) =>
|
|
1354
|
+
(key.startsWith('device.') === true ||
|
|
1355
|
+
(key.startsWith('DEVICE_') === true && PROTOBUF_THERMOSTAT_RESOURCES.includes(value.value?.device_info?.typeName) === true)) &&
|
|
1356
|
+
(deviceUUID === '' || deviceUUID === key),
|
|
1357
|
+
)
|
|
1358
|
+
.forEach(([object_key, value]) => {
|
|
1359
|
+
let tempDevice = {};
|
|
1360
|
+
try {
|
|
1361
|
+
if (value.source === NestAccfactory.DataSource.PROTOBUF) {
|
|
1362
|
+
let RESTTypeData = {};
|
|
1363
|
+
RESTTypeData.mac_address = Buffer.from(value.value.wifi_interface.macAddress, 'base64');
|
|
1364
|
+
RESTTypeData.serial_number = value.value.device_identity.serialNumber;
|
|
1365
|
+
RESTTypeData.software_version = value.value.device_identity.softwareVersion;
|
|
1366
|
+
RESTTypeData.model = 'Thermostat';
|
|
1367
|
+
if (value.value.device_info.typeName === 'nest.resource.NestLearningThermostat3Resource') {
|
|
1368
|
+
RESTTypeData.model = 'Learning Thermostat (3rd gen)';
|
|
1369
|
+
}
|
|
1370
|
+
if (value.value.device_info.typeName === 'google.resource.GoogleBismuth1Resource') {
|
|
1371
|
+
RESTTypeData.model = 'Learning Thermostat (4th gen)';
|
|
1372
|
+
}
|
|
1373
|
+
if (value.value.device_info.typeName === 'nest.resource.NestAgateDisplayResource') {
|
|
1374
|
+
RESTTypeData.model = 'Thermostat E';
|
|
1375
|
+
}
|
|
1376
|
+
if (value.value.device_info.typeName === 'nest.resource.NestOnyxResource') {
|
|
1377
|
+
RESTTypeData.model = 'Thermostat E (1st gen)';
|
|
1378
|
+
}
|
|
1379
|
+
if (value.value.device_info.typeName === 'google.resource.GoogleZirconium1Resource') {
|
|
1380
|
+
RESTTypeData.model = 'Thermostat (2020 Model)';
|
|
1381
|
+
}
|
|
1382
|
+
RESTTypeData.current_humidity =
|
|
1383
|
+
typeof value.value.current_humidity.humidityValue.humidity.value === 'number'
|
|
1384
|
+
? value.value.current_humidity.humidityValue.humidity.value
|
|
1385
|
+
: 0.0;
|
|
1386
|
+
RESTTypeData.temperature_scale = value.value.display_settings.temperatureScale === 'TEMPERATURE_SCALE_F' ? 'F' : 'C';
|
|
1387
|
+
RESTTypeData.removed_from_base = value.value.display.thermostatState.includes('bpd') === true;
|
|
1388
|
+
RESTTypeData.backplate_temperature = parseFloat(value.value.backplate_temperature.temperatureValue.temperature.value);
|
|
1389
|
+
RESTTypeData.current_temperature = parseFloat(value.value.current_temperature.temperatureValue.temperature.value);
|
|
1390
|
+
RESTTypeData.battery_level = parseFloat(value.value.battery_voltage.batteryValue.batteryVoltage.value);
|
|
1391
|
+
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
1392
|
+
RESTTypeData.leaf = value.value?.leaf?.active === true;
|
|
1393
|
+
RESTTypeData.has_humidifier = value.value.hvac_equipment_capabilities.hasHumidifier === true;
|
|
1394
|
+
RESTTypeData.has_dehumidifier = value.value.hvac_equipment_capabilities.hasDehumidifier === true;
|
|
1395
|
+
RESTTypeData.has_fan =
|
|
1396
|
+
typeof value.value.fan_control_capabilities.maxAvailableSpeed === 'string' &&
|
|
1397
|
+
value.value.fan_control_capabilities.maxAvailableSpeed !== 'FAN_SPEED_SETTING_OFF'
|
|
1398
|
+
? true
|
|
1399
|
+
: false;
|
|
1400
|
+
RESTTypeData.can_cool =
|
|
1401
|
+
value.value.hvac_equipment_capabilities.hasStage1Cool === true ||
|
|
1402
|
+
value.value.hvac_equipment_capabilities.hasStage2Cool === true ||
|
|
1403
|
+
value.value.hvac_equipment_capabilities.hasStage3Cool === true;
|
|
1404
|
+
RESTTypeData.can_heat =
|
|
1405
|
+
value.value.hvac_equipment_capabilities.hasStage1Heat === true ||
|
|
1406
|
+
value.value.hvac_equipment_capabilities.hasStage2Heat === true ||
|
|
1407
|
+
value.value.hvac_equipment_capabilities.hasStage3Heat === true;
|
|
1408
|
+
RESTTypeData.temperature_lock = value.value.temperature_lock_settings.enabled === true;
|
|
1409
|
+
RESTTypeData.temperature_lock_pin_hash =
|
|
1410
|
+
typeof value.value.temperature_lock_settings.pinHash === 'string' && value.value.temperature_lock_settings.enabled === true
|
|
1411
|
+
? value.value.temperature_lock_settings.pinHash
|
|
1412
|
+
: '';
|
|
1413
|
+
RESTTypeData.away = value.value.structure_mode.structureMode === 'STRUCTURE_MODE_AWAY';
|
|
1414
|
+
RESTTypeData.occupancy = value.value.structure_mode.structureMode === 'STRUCTURE_MODE_HOME';
|
|
1415
|
+
//RESTTypeData.occupancy = (value.value.structure_mode.occupancy.activity === 'ACTIVITY_ACTIVE');
|
|
1416
|
+
RESTTypeData.vacation_mode = value.value.structure_mode.structureMode === 'STRUCTURE_MODE_VACATION';
|
|
1417
|
+
RESTTypeData.description = typeof value.value.label?.label === 'string' ? value.value.label.label : '';
|
|
1418
|
+
RESTTypeData.location = get_location_name(
|
|
1419
|
+
value.value.device_info.pairerId.resourceId,
|
|
1420
|
+
value.value.device_located_settings.whereAnnotationRid.resourceId,
|
|
1421
|
+
);
|
|
1422
|
+
|
|
1423
|
+
// Work out current mode. ie: off, cool, heat, range and get temperature low/high and target
|
|
1424
|
+
RESTTypeData.hvac_mode =
|
|
1425
|
+
value.value.target_temperature_settings.enabled.value === true
|
|
1426
|
+
? value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase()
|
|
1427
|
+
: 'off';
|
|
1428
|
+
RESTTypeData.target_temperature_low =
|
|
1429
|
+
typeof value.value.target_temperature_settings.targetTemperature.heatingTarget.value === 'number'
|
|
1430
|
+
? value.value.target_temperature_settings.targetTemperature.heatingTarget.value
|
|
1431
|
+
: 0.0;
|
|
1432
|
+
RESTTypeData.target_temperature_high =
|
|
1433
|
+
typeof value.value.target_temperature_settings.targetTemperature.coolingTarget.value === 'number'
|
|
1434
|
+
? value.value.target_temperature_settings.targetTemperature.coolingTarget.value
|
|
1435
|
+
: 0.0;
|
|
1436
|
+
if (value.value.target_temperature_settings.targetTemperature.setpointType === 'SET_POINT_TYPE_COOL') {
|
|
1437
|
+
// Target temperature is the cooling point
|
|
1438
|
+
RESTTypeData.target_temperature = value.value.target_temperature_settings.targetTemperature.coolingTarget.value;
|
|
1439
|
+
}
|
|
1440
|
+
if (value.value.target_temperature_settings.targetTemperature.setpointType === 'SET_POINT_TYPE_HEAT') {
|
|
1441
|
+
// Target temperature is the heating point
|
|
1442
|
+
RESTTypeData.target_temperature = value.value.target_temperature_settings.targetTemperature.heatingTarget.value;
|
|
1443
|
+
}
|
|
1444
|
+
if (value.value.target_temperature_settings.targetTemperature.setpointType === 'SET_POINT_TYPE_RANGE') {
|
|
1445
|
+
// Target temperature is in between the heating and cooling point
|
|
1446
|
+
RESTTypeData.target_temperature =
|
|
1447
|
+
(value.value.target_temperature_settings.targetTemperature.coolingTarget.value +
|
|
1448
|
+
value.value.target_temperature_settings.targetTemperature.heatingTarget.value) *
|
|
1449
|
+
0.5;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Work out if eco mode is active and adjust temperature low/high and target
|
|
1453
|
+
if (value.value.eco_mode_state.ecoMode !== 'ECO_MODE_INACTIVE') {
|
|
1454
|
+
RESTTypeData.target_temperature_low = value.value.eco_mode_settings.ecoTemperatureHeat.value.value;
|
|
1455
|
+
RESTTypeData.target_temperature_high = value.value.eco_mode_settings.ecoTemperatureCool.value.value;
|
|
1456
|
+
if (
|
|
1457
|
+
value.value.eco_mode_settings.ecoTemperatureHeat.enabled === true &&
|
|
1458
|
+
value.value.eco_mode_settings.ecoTemperatureCool.enabled === false
|
|
1459
|
+
) {
|
|
1460
|
+
RESTTypeData.target_temperature = value.value.eco_mode_settings.ecoTemperatureHeat.value.value;
|
|
1461
|
+
RESTTypeData.hvac_mode = 'ecoheat';
|
|
1462
|
+
}
|
|
1463
|
+
if (
|
|
1464
|
+
value.value.eco_mode_settings.ecoTemperatureHeat.enabled === false &&
|
|
1465
|
+
value.value.eco_mode_settings.ecoTemperatureCool.enabled === true
|
|
1466
|
+
) {
|
|
1467
|
+
RESTTypeData.target_temperature = value.value.eco_mode_settings.ecoTemperatureCool.value.value;
|
|
1468
|
+
RESTTypeData.hvac_mode = 'ecocool';
|
|
1469
|
+
}
|
|
1470
|
+
if (
|
|
1471
|
+
value.value.eco_mode_settings.ecoTemperatureHeat.enabled === true &&
|
|
1472
|
+
value.value.eco_mode_settings.ecoTemperatureCool.enabled === true
|
|
1473
|
+
) {
|
|
1474
|
+
RESTTypeData.target_temperature =
|
|
1475
|
+
(value.value.eco_mode_settings.ecoTemperatureCool.value.value +
|
|
1476
|
+
value.value.eco_mode_settings.ecoTemperatureHeat.value.value) *
|
|
1477
|
+
0.5;
|
|
1478
|
+
RESTTypeData.hvac_mode = 'ecorange';
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// Work out current state ie: heating, cooling etc
|
|
1483
|
+
RESTTypeData.hvac_state = 'off'; // By default, we're not heating or cooling
|
|
1484
|
+
if (
|
|
1485
|
+
value.value.hvac_control.hvacState.coolStage1Active === true ||
|
|
1486
|
+
value.value.hvac_control.hvacState.coolStage2Active === true ||
|
|
1487
|
+
value.value.hvac_control.hvacState.coolStage2Active === true
|
|
1488
|
+
) {
|
|
1489
|
+
// A cooling source is on, so we're in cooling mode
|
|
1490
|
+
RESTTypeData.hvac_state = 'cooling';
|
|
1491
|
+
}
|
|
1492
|
+
if (
|
|
1493
|
+
value.value.hvac_control.hvacState.heatStage1Active === true ||
|
|
1494
|
+
value.value.hvac_control.hvacState.heatStage2Active === true ||
|
|
1495
|
+
value.value.hvac_control.hvacState.heatStage3Active === true ||
|
|
1496
|
+
value.value.hvac_control.hvacState.alternateHeatStage1Active === true ||
|
|
1497
|
+
value.value.hvac_control.hvacState.alternateHeatStage2Active === true ||
|
|
1498
|
+
value.value.hvac_control.hvacState.auxiliaryHeatActive === true ||
|
|
1499
|
+
value.value.hvac_control.hvacState.emergencyHeatActive === true
|
|
1500
|
+
) {
|
|
1501
|
+
// A heating source is on, so we're in heating mode
|
|
1502
|
+
RESTTypeData.hvac_state = 'heating';
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Update fan status, on or off and max number of speeds supported
|
|
1506
|
+
RESTTypeData.fan_state = parseInt(value.value.fan_control_settings.timerEnd?.seconds) > 0 ? true : false;
|
|
1507
|
+
RESTTypeData.fan_current_speed =
|
|
1508
|
+
value.value.fan_control_settings.timerSpeed.includes('FAN_SPEED_SETTING_STAGE') === true
|
|
1509
|
+
? parseInt(value.value.fan_control_settings.timerSpeed.split('FAN_SPEED_SETTING_STAGE')[1])
|
|
1510
|
+
: 0;
|
|
1511
|
+
RESTTypeData.fan_max_speed =
|
|
1512
|
+
value.value.fan_control_capabilities.maxAvailableSpeed.includes('FAN_SPEED_SETTING_STAGE') === true
|
|
1513
|
+
? parseInt(value.value.fan_control_capabilities.maxAvailableSpeed.split('FAN_SPEED_SETTING_STAGE')[1])
|
|
1514
|
+
: 0;
|
|
1515
|
+
|
|
1516
|
+
// Humidifier/dehumidifier details
|
|
1517
|
+
RESTTypeData.target_humidity = value.value.humidity_control_settings.targetHumidity.value;
|
|
1518
|
+
RESTTypeData.humidifier_state = value.value.hvac_control.hvacState.humidifierActive === true;
|
|
1519
|
+
RESTTypeData.dehumidifier_state = value.value.hvac_control.hvacState.dehumidifierActive === true;
|
|
1520
|
+
|
|
1521
|
+
// Air filter details
|
|
1522
|
+
RESTTypeData.has_air_filter = value.value.hvac_equipment_capabilities.hasAirFilter === true;
|
|
1523
|
+
RESTTypeData.filter_replacement_needed = value.value.filter_reminder.filterReplacementNeeded.value === true;
|
|
1524
|
+
|
|
1525
|
+
// Process any temperature sensors associated with this thermostat
|
|
1526
|
+
RESTTypeData.active_rcs_sensor =
|
|
1527
|
+
typeof value.value.remote_comfort_sensing_settings?.activeRcsSelection?.activeRcsSensor === 'string'
|
|
1528
|
+
? value.value.remote_comfort_sensing_settings.activeRcsSelection.activeRcsSensor.resourceId
|
|
1529
|
+
: '';
|
|
1530
|
+
RESTTypeData.linked_rcs_sensors = [];
|
|
1531
|
+
if (typeof value.value?.remote_comfort_sensing_settings?.associatedRcsSensors === 'object') {
|
|
1532
|
+
value.value.remote_comfort_sensing_settings.associatedRcsSensors.forEach((sensor) => {
|
|
1533
|
+
if (typeof this.#rawData?.[sensor.deviceId.resourceId]?.value === 'object') {
|
|
1534
|
+
this.#rawData[sensor.deviceId.resourceId].value.associated_thermostat = object_key; // Sensor is linked to this thermostat
|
|
1535
|
+
|
|
1536
|
+
// Get sensor online/offline status
|
|
1537
|
+
// 'liveness' property doesn't appear in protobuf data for temp sensors, so we'll add that object here
|
|
1538
|
+
this.#rawData[sensor.deviceId.resourceId].value.liveness = {};
|
|
1539
|
+
this.#rawData[sensor.deviceId.resourceId].value.liveness.status = 'LIVENESS_DEVICE_STATUS_UNSPECIFIED';
|
|
1540
|
+
if (typeof value.value?.remote_comfort_sensing_state?.rcsSensorStatuses === 'object') {
|
|
1541
|
+
Object.values(value.value.remote_comfort_sensing_state.rcsSensorStatuses).forEach((sensorStatus) => {
|
|
1542
|
+
if (
|
|
1543
|
+
sensorStatus?.sensorId?.resourceId === sensor.deviceId.resourceId &&
|
|
1544
|
+
sensorStatus?.dataRecency?.includes('OK') === true
|
|
1545
|
+
) {
|
|
1546
|
+
this.#rawData[sensor.deviceId.resourceId].value.liveness.status = 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
RESTTypeData.linked_rcs_sensors.push(sensor.deviceId.resourceId);
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
RESTTypeData.schedule_mode =
|
|
1557
|
+
value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase() !== 'off'
|
|
1558
|
+
? value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase()
|
|
1559
|
+
: '';
|
|
1560
|
+
RESTTypeData.schedules = {};
|
|
1561
|
+
|
|
1562
|
+
if (
|
|
1563
|
+
typeof value.value[RESTTypeData.schedule_mode + '_schedule_settings'].setpoints === 'object' &&
|
|
1564
|
+
value.value[RESTTypeData.schedule_mode + '_schedule_settings'].type ===
|
|
1565
|
+
'SET_POINT_SCHEDULE_TYPE_' + RESTTypeData.schedule_mode.toUpperCase()
|
|
1566
|
+
) {
|
|
1567
|
+
Object.values(value.value[RESTTypeData.schedule_mode + '_schedule_settings'].setpoints).forEach((schedule) => {
|
|
1568
|
+
// Create REST API schedule entries
|
|
1569
|
+
const DAYSOFWEEK = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'];
|
|
1570
|
+
let dayofWeekIndex = DAYSOFWEEK.indexOf(schedule.dayOfWeek.split('DAY_OF_WEEK_')[1]);
|
|
1571
|
+
|
|
1572
|
+
if (typeof RESTTypeData.schedules[dayofWeekIndex] === 'undefined') {
|
|
1573
|
+
RESTTypeData.schedules[dayofWeekIndex] = {};
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
RESTTypeData.schedules[dayofWeekIndex][Object.entries(RESTTypeData.schedules[dayofWeekIndex]).length] = {
|
|
1577
|
+
'temp-min': adjustTemperature(schedule.heatingTarget.value, 'C', 'C', true),
|
|
1578
|
+
'temp-max': adjustTemperature(schedule.coolingTarget.value, 'C', 'C', true),
|
|
1579
|
+
time: typeof schedule.secondsInDay === 'number' ? schedule.secondsInDay : 0,
|
|
1580
|
+
type: RESTTypeData.schedule_mode.toUpperCase(),
|
|
1581
|
+
entry_type: 'setpoint',
|
|
1582
|
+
};
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
tempDevice = process_thermostat_data(object_key, RESTTypeData);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (value.source === NestAccfactory.DataSource.REST) {
|
|
1590
|
+
let RESTTypeData = {};
|
|
1591
|
+
RESTTypeData.mac_address = value.value.mac_address;
|
|
1592
|
+
RESTTypeData.serial_number = value.value.serial_number;
|
|
1593
|
+
RESTTypeData.software_version = value.value.current_version;
|
|
1594
|
+
RESTTypeData.model = 'Thermostat';
|
|
1595
|
+
if (value.value.serial_number.serial_number.substring(0, 2) === '15') {
|
|
1596
|
+
RESTTypeData.model = 'Thermostat E (1st gen)'; // Nest Thermostat E
|
|
1597
|
+
}
|
|
1598
|
+
if (value.value.serial_number.serial_number.substring(0, 2) === '09') {
|
|
1599
|
+
RESTTypeData.model = 'Thermostat (3rd gen)'; // Nest Thermostat 3rd Gen
|
|
1600
|
+
}
|
|
1601
|
+
if (value.value.serial_number.serial_number.substring(0, 2) === '02') {
|
|
1602
|
+
RESTTypeData.model = 'Thermostat (2nd gen)'; // Nest Thermostat 2nd Gen
|
|
1603
|
+
}
|
|
1604
|
+
if (value.value.serial_number.serial_number.substring(0, 2) === '01') {
|
|
1605
|
+
RESTTypeData.model = 'Thermostat (1st gen)'; // Nest Thermostat 1st Gen
|
|
1606
|
+
}
|
|
1607
|
+
RESTTypeData.current_humidity = value.value.current_humidity;
|
|
1608
|
+
RESTTypeData.temperature_scale = value.value.temperature_scale;
|
|
1609
|
+
RESTTypeData.removed_from_base = value.value.nlclient_state.toUpperCase() === 'BPD';
|
|
1610
|
+
RESTTypeData.backplate_temperature = value.value.backplate_temperature;
|
|
1611
|
+
RESTTypeData.current_temperature = value.value.backplate_temperature;
|
|
1612
|
+
RESTTypeData.battery_level = value.value.battery_level;
|
|
1613
|
+
RESTTypeData.online = this.#rawData['track.' + value.value.serial_number].value.online === true;
|
|
1614
|
+
RESTTypeData.leaf = value.value.leaf === true;
|
|
1615
|
+
RESTTypeData.has_humidifier = value.value.has_humidifier === true;
|
|
1616
|
+
RESTTypeData.has_dehumidifier = value.value.has_dehumidifier === true;
|
|
1617
|
+
RESTTypeData.has_fan = value.value.has_fan === true;
|
|
1618
|
+
RESTTypeData.can_cool = this.#rawData['shared.' + value.value.serial_number].value.can_cool === true;
|
|
1619
|
+
RESTTypeData.can_heat = this.#rawData['shared.' + value.value.serial_number].value.can_heat === true;
|
|
1620
|
+
RESTTypeData.temperature_lock = value.value.temperature_lock === true;
|
|
1621
|
+
RESTTypeData.temperature_lock_pin_hash = value.value.temperature_lock_pin_hash;
|
|
1622
|
+
RESTTypeData.away = false;
|
|
1623
|
+
if (
|
|
1624
|
+
typeof this.#rawData['structure.' + this.#rawData['link.' + value.value.serial_number].value.structure.split('.')[1]]?.value
|
|
1625
|
+
?.away === 'boolean'
|
|
1626
|
+
) {
|
|
1627
|
+
RESTTypeData.away =
|
|
1628
|
+
this.#rawData['structure.' + this.#rawData['link.' + value.value.serial_number].value.structure.split('.')[1]].value.away;
|
|
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
|
+
}
|
|
1636
|
+
RESTTypeData.occupancy = RESTTypeData.away === false; // Occupancy is opposite of away status ie: away is false, then occupied
|
|
1637
|
+
RESTTypeData.vacation_mode = false;
|
|
1638
|
+
if (
|
|
1639
|
+
typeof this.#rawData['structure.' + this.#rawData['link.' + value.value.serial_number].value.structure.split('.')[1]]?.value
|
|
1640
|
+
?.vacation_mode === 'boolean'
|
|
1641
|
+
) {
|
|
1642
|
+
RESTTypeData.vacation_mode =
|
|
1643
|
+
this.#rawData[
|
|
1644
|
+
'structure.' + this.#rawData['link.' + value.value.serial_number].value.structure.split('.')[1]
|
|
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
|
+
}
|
|
1653
|
+
RESTTypeData.description =
|
|
1654
|
+
typeof this.#rawData['shared.' + value.value.serial_number]?.value?.name === 'string'
|
|
1655
|
+
? makeHomeKitName(this.#rawData['shared.' + value.value.serial_number].value.name)
|
|
1656
|
+
: '';
|
|
1657
|
+
RESTTypeData.location = get_location_name(
|
|
1658
|
+
this.#rawData['link.' + value.value.serial_number].value.structure.split('.')[1],
|
|
1659
|
+
value.value.where_id,
|
|
1660
|
+
);
|
|
1661
|
+
|
|
1662
|
+
// 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].value.target_temperature_type.toUpperCase() === 'COOL') {
|
|
1667
|
+
// Target temperature is the cooling point
|
|
1668
|
+
RESTTypeData.target_temperature = this.#rawData['shared.' + value.value.serial_number].value.target_temperature_high;
|
|
1669
|
+
}
|
|
1670
|
+
if (this.#rawData['shared.' + value.value.serial_number].value.target_temperature_type.toUpperCase() === 'HEAT') {
|
|
1671
|
+
// Target temperature is the heating point
|
|
1672
|
+
RESTTypeData.target_temperature = this.#rawData['shared.' + value.value.serial_number].value.target_temperature_low;
|
|
1673
|
+
}
|
|
1674
|
+
if (this.#rawData['shared.' + value.value.serial_number].value.target_temperature_type.toUpperCase() === 'RANGE') {
|
|
1675
|
+
// Target temperature is in between the heating and cooling point
|
|
1676
|
+
RESTTypeData.target_temperature =
|
|
1677
|
+
(this.#rawData['shared.' + value.value.serial_number].value.target_temperature_low +
|
|
1678
|
+
this.#rawData['shared.' + value.value.serial_number].value.target_temperature_high) *
|
|
1679
|
+
0.5;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Work out if eco mode is active and adjust temperature low/high and target
|
|
1683
|
+
if (value.value.eco.mode.toUpperCase() === 'AUTO-ECO' || value.value.eco.mode.toUpperCase() === 'MANUAL-ECO') {
|
|
1684
|
+
RESTTypeData.target_temperature_low = value.value.away_temperature_low;
|
|
1685
|
+
RESTTypeData.target_temperature_high = value.value.away_temperature_high;
|
|
1686
|
+
if (value.value.away_temperature_high_enabled === true && value.value.away_temperature_low_enabled === false) {
|
|
1687
|
+
RESTTypeData.target_temperature = value.value.away_temperature_low;
|
|
1688
|
+
RESTTypeData.hvac_mode = 'ecoheat';
|
|
1689
|
+
}
|
|
1690
|
+
if (value.value.away_temperature_high_enabled === true && value.value.away_temperature_low_enabled === false) {
|
|
1691
|
+
RESTTypeData.target_temperature = value.value.away_temperature_high;
|
|
1692
|
+
RESTTypeData.hvac_mode = 'ecocool';
|
|
1693
|
+
}
|
|
1694
|
+
if (value.value.away_temperature_high_enabled === true && value.value.away_temperature_low_enabled === true) {
|
|
1695
|
+
RESTTypeData.target_temperature = (value.value.away_temperature_low + value.value.away_temperature_high) * 0.5;
|
|
1696
|
+
RESTTypeData.hvac_mode = 'ecorange';
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// Work out current state ie: heating, cooling etc
|
|
1701
|
+
RESTTypeData.hvac_state = 'off'; // By default, we're not heating or cooling
|
|
1702
|
+
if (
|
|
1703
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_heater_state === true ||
|
|
1704
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_heat_x2_state === true ||
|
|
1705
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_heat_x3_state === true ||
|
|
1706
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_aux_heater_state === true ||
|
|
1707
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_alt_heat_x2_state === true ||
|
|
1708
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_emer_heat_state === true ||
|
|
1709
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_alt_heat_state === true
|
|
1710
|
+
) {
|
|
1711
|
+
// A heating source is on, so we're in heating mode
|
|
1712
|
+
RESTTypeData.hvac_state = 'heating';
|
|
1713
|
+
}
|
|
1714
|
+
if (
|
|
1715
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_ac_state === true ||
|
|
1716
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_cool_x2_state === true ||
|
|
1717
|
+
this.#rawData['shared.' + value.value.serial_number].value.hvac_cool_x3_state === true
|
|
1718
|
+
) {
|
|
1719
|
+
// A cooling source is on, so we're in cooling mode
|
|
1720
|
+
RESTTypeData.hvac_state = 'cooling';
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Update fan status, on or off
|
|
1724
|
+
RESTTypeData.fan_state = value.value.fan_timer_timeout > 0 ? true : false;
|
|
1725
|
+
RESTTypeData.fan_current_speed =
|
|
1726
|
+
value.value.fan_timer_speed.includes('stage') === true ? parseInt(value.value.fan_timer_speed.split('stage')[1]) : 0;
|
|
1727
|
+
RESTTypeData.fan_max_speed =
|
|
1728
|
+
value.value.fan_capabilities.includes('stage') === true ? parseInt(value.value.fan_capabilities.split('stage')[1]) : 0;
|
|
1729
|
+
|
|
1730
|
+
// Humidifier/dehumidifier details
|
|
1731
|
+
RESTTypeData.target_humidity = typeof value.value.target_humidity === 'number' ? value.value.target_humidity : 0.0;
|
|
1732
|
+
RESTTypeData.humidifier_state = value.value.humidifier_state === true;
|
|
1733
|
+
RESTTypeData.dehumidifier_state = value.value.dehumidifier_state === true;
|
|
1734
|
+
|
|
1735
|
+
// Air filter details
|
|
1736
|
+
RESTTypeData.has_air_filter = value.value.has_air_filter === true;
|
|
1737
|
+
RESTTypeData.filter_replacement_needed = value.value.filter_replacement_needed === true;
|
|
1738
|
+
|
|
1739
|
+
// Process any temperature sensors associated with this thermostat
|
|
1740
|
+
RESTTypeData.active_rcs_sensor = '';
|
|
1741
|
+
RESTTypeData.linked_rcs_sensors = [];
|
|
1742
|
+
this.#rawData['rcs_settings.' + value.value.serial_number].value.associated_rcs_sensors.forEach((sensor) => {
|
|
1743
|
+
if (typeof this.#rawData[sensor]?.value === 'object') {
|
|
1744
|
+
this.#rawData[sensor].value.associated_thermostat = object_key; // Sensor is linked to this thermostat
|
|
1745
|
+
|
|
1746
|
+
// Is this sensor the active one? If so, get some details about it
|
|
1747
|
+
if (this.#rawData['rcs_settings.' + value.value.serial_number].value.active_rcs_sensors.includes(sensor)) {
|
|
1748
|
+
RESTTypeData.active_rcs_sensor = this.#rawData[sensor].value.serial_number.toUpperCase();
|
|
1749
|
+
RESTTypeData.current_temperature = this.#rawData[sensor].value.current_temperature;
|
|
1750
|
+
}
|
|
1751
|
+
RESTTypeData.linked_rcs_sensors.push(this.#rawData[sensor].value.serial_number.toUpperCase());
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
// Get associated schedules
|
|
1756
|
+
if (typeof this.#rawData['schedule.' + value.value.serial_number] === 'object') {
|
|
1757
|
+
Object.values(this.#rawData['schedule.' + value.value.serial_number].value.days).forEach((schedules) => {
|
|
1758
|
+
Object.values(schedules).forEach((schedule) => {
|
|
1759
|
+
// Fix up temperatures in the schedule
|
|
1760
|
+
if (typeof schedule['temp'] === 'number') {
|
|
1761
|
+
schedule.temp = adjustTemperature(schedule.temp, 'C', 'C', true);
|
|
1762
|
+
}
|
|
1763
|
+
if (typeof schedule['temp-min'] === 'number') {
|
|
1764
|
+
schedule['temp-min'] = adjustTemperature(schedule['temp-min'], 'C', 'C', true);
|
|
1765
|
+
}
|
|
1766
|
+
if (typeof schedule['temp-max'] === 'number') {
|
|
1767
|
+
schedule['temp-max'] = adjustTemperature(schedule['temp-max'], 'C', 'C', true);
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
});
|
|
1771
|
+
RESTTypeData.schedules = this.#rawData['schedule.' + value.value.serial_number].value.days;
|
|
1772
|
+
RESTTypeData.schedule_mode = this.#rawData['schedule.' + value.value.serial_number].value.schedule_mode;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
tempDevice = process_thermostat_data(object_key, RESTTypeData);
|
|
1776
|
+
}
|
|
1777
|
+
// eslint-disable-next-line no-unused-vars
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
this?.log?.debug && this.log.debug('Error processing data for thermostat(s)');
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
|
|
1783
|
+
// Insert any extra options we've read in from configuration file for this device
|
|
1784
|
+
tempDevice.eveHistory =
|
|
1785
|
+
this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
|
|
1786
|
+
tempDevice.humiditySensor = this.config?.devices?.[tempDevice.serial_number]?.humiditySensor === true;
|
|
1787
|
+
tempDevice.externalCool =
|
|
1788
|
+
typeof this.config?.devices?.[tempDevice.serial_number]?.externalCool === 'string'
|
|
1789
|
+
? this.config.devices[tempDevice.serial_number].externalCool
|
|
1790
|
+
: undefined; // Config option for external cooling source
|
|
1791
|
+
tempDevice.externalHeat =
|
|
1792
|
+
typeof this.config?.devices?.[tempDevice.serial_number]?.externalHeat === 'string'
|
|
1793
|
+
? this.config.devices[tempDevice.serial_number].externalHeat
|
|
1794
|
+
: undefined; // Config option for external heating source
|
|
1795
|
+
tempDevice.externalFan =
|
|
1796
|
+
typeof this.config?.devices?.[tempDevice.serial_number]?.externalFan === 'string'
|
|
1797
|
+
? this.config.devices[tempDevice.serial_number].externalFan
|
|
1798
|
+
: undefined; // Config option for external fan source
|
|
1799
|
+
tempDevice.externalDehumidifier =
|
|
1800
|
+
typeof this.config?.devices?.[tempDevice.serial_number]?.externalDehumidifier === 'string'
|
|
1801
|
+
? this.config.devices[tempDevice.serial_number].externalDehumidifier
|
|
1802
|
+
: undefined; // Config option for external dehumidifier source
|
|
1803
|
+
devices[tempDevice.serial_number] = tempDevice; // Store processed device
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
// Process data for any temperature sensors we have in the raw data
|
|
1808
|
+
// This is done AFTER where have processed thermostat(s) as we inserted some extra details in there
|
|
1809
|
+
// We only process if the sensor has been associated to a thermostat
|
|
1810
|
+
const process_kryptonite_data = (object_key, data) => {
|
|
1811
|
+
let processed = {};
|
|
1812
|
+
try {
|
|
1813
|
+
// Fix up data we need to
|
|
1814
|
+
data.serial_number = data.serial_number.toUpperCase();
|
|
1815
|
+
data.excluded =
|
|
1816
|
+
typeof this.config?.devices?.[data.serial_number]?.exclude === 'boolean'
|
|
1817
|
+
? this.config.devices[data.serial_number].exclude
|
|
1818
|
+
: false; // Mark device as excluded or not
|
|
1819
|
+
data.device_type = NestAccfactory.DeviceType.TEMPSENSOR; // Nest Temperature sensor
|
|
1820
|
+
data.uuid = object_key; // Internal structure ID
|
|
1821
|
+
data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
|
|
1822
|
+
data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
|
|
1823
|
+
data.model = 'Temperature Sensor';
|
|
1824
|
+
data.current_temperature = adjustTemperature(data.current_temperature, 'C', 'C', true);
|
|
1825
|
+
let description = typeof data?.description === 'string' ? data.description : '';
|
|
1826
|
+
let location = typeof data?.location === 'string' ? data.location : '';
|
|
1827
|
+
if (description === '') {
|
|
1828
|
+
description = location;
|
|
1829
|
+
location = '';
|
|
1830
|
+
}
|
|
1831
|
+
data.description = makeHomeKitName(location === '' ? description : description + ' - ' + location);
|
|
1832
|
+
delete data.location;
|
|
1833
|
+
|
|
1834
|
+
// Insert details for when using HAP-NodeJS library rather than Homebridge
|
|
1835
|
+
if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
|
|
1836
|
+
data.hkPairingCode = this.config.options.hkPairingCode;
|
|
1837
|
+
}
|
|
1838
|
+
if (
|
|
1839
|
+
typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
|
|
1840
|
+
this.config.devices[data.serial_number].hkPairingCode !== ''
|
|
1841
|
+
) {
|
|
1842
|
+
data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
|
|
1843
|
+
}
|
|
1844
|
+
if (data?.hkPairingCode !== undefined) {
|
|
1845
|
+
// Use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off serial number for last 6 digits.
|
|
1846
|
+
let tempMACAddress = '18B430' + crc24(data.serial_number).toUpperCase();
|
|
1847
|
+
data.hkUsername = tempMACAddress
|
|
1848
|
+
.toString('hex')
|
|
1849
|
+
.split(/(..)/)
|
|
1850
|
+
.filter((s) => s)
|
|
1851
|
+
.join(':')
|
|
1852
|
+
.toUpperCase(); // Create mac_address in format of xx:xx:xx:xx:xx:xx
|
|
1853
|
+
}
|
|
1854
|
+
delete data.mac_address;
|
|
1855
|
+
|
|
1856
|
+
processed = data;
|
|
1857
|
+
// eslint-disable-next-line no-unused-vars
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
// Empty
|
|
1860
|
+
}
|
|
1861
|
+
return processed;
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1864
|
+
Object.entries(this.#rawData)
|
|
1865
|
+
.filter(
|
|
1866
|
+
([key, value]) =>
|
|
1867
|
+
(key.startsWith('kryptonite.') === true ||
|
|
1868
|
+
(key.startsWith('DEVICE_') === true && value.value?.device_info?.typeName === 'nest.resource.NestKryptoniteResource')) &&
|
|
1869
|
+
(deviceUUID === '' || deviceUUID === key),
|
|
1870
|
+
)
|
|
1871
|
+
.forEach(([object_key, value]) => {
|
|
1872
|
+
let tempDevice = {};
|
|
1873
|
+
try {
|
|
1874
|
+
if (
|
|
1875
|
+
value.source === NestAccfactory.DataSource.PROTOBUF &&
|
|
1876
|
+
typeof value?.value?.associated_thermostat === 'string' &&
|
|
1877
|
+
value?.value?.associated_thermostat !== ''
|
|
1878
|
+
) {
|
|
1879
|
+
let RESTTypeData = {};
|
|
1880
|
+
RESTTypeData.serial_number = value.value.device_identity.serialNumber;
|
|
1881
|
+
// Guessing battery minimum voltage is 2v??
|
|
1882
|
+
RESTTypeData.battery_level = scaleValue(value.value.battery.assessedVoltage.value, 2.0, 3.0, 0, 100);
|
|
1883
|
+
RESTTypeData.current_temperature = value.value.current_temperature.temperatureValue.temperature.value;
|
|
1884
|
+
// Online status we 'faked' when processing Thermostat protobuf data
|
|
1885
|
+
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
1886
|
+
RESTTypeData.associated_thermostat = value.value.associated_thermostat;
|
|
1887
|
+
RESTTypeData.description = typeof value.value?.label?.label === 'string' ? value.value.label.label : '';
|
|
1888
|
+
RESTTypeData.location = get_location_name(
|
|
1889
|
+
value.value.device_info.pairerId.resourceId,
|
|
1890
|
+
value.value.device_located_settings.whereAnnotationRid.resourceId,
|
|
1891
|
+
);
|
|
1892
|
+
RESTTypeData.active_sensor =
|
|
1893
|
+
this.#rawData[value.value.associated_thermostat].value?.remote_comfort_sensing_settings?.activeRcsSelection?.activeRcsSensor
|
|
1894
|
+
?.resourceId === object_key;
|
|
1895
|
+
tempDevice = process_kryptonite_data(object_key, RESTTypeData);
|
|
1896
|
+
}
|
|
1897
|
+
if (
|
|
1898
|
+
value.source === NestAccfactory.DataSource.REST &&
|
|
1899
|
+
typeof value?.value?.associated_thermostat === 'string' &&
|
|
1900
|
+
value?.value?.associated_thermostat !== ''
|
|
1901
|
+
) {
|
|
1902
|
+
let RESTTypeData = {};
|
|
1903
|
+
RESTTypeData.serial_number = value.value.sserial_number;
|
|
1904
|
+
RESTTypeData.battery_level = scaleValue(value.value.battery_level, 0, 100, 0, 100);
|
|
1905
|
+
RESTTypeData.current_temperature = value.value.current_temperature;
|
|
1906
|
+
RESTTypeData.online = Math.floor(Date.now() / 1000) - value.value.last_updated_at < 3600 * 4 ? true : false;
|
|
1907
|
+
RESTTypeData.associated_thermostat = value.value.associated_thermostat;
|
|
1908
|
+
RESTTypeData.description = value.value.description;
|
|
1909
|
+
RESTTypeData.location = get_location_name(value.value.structure_id, value.value.where_id);
|
|
1910
|
+
RESTTypeData.active_sensor =
|
|
1911
|
+
this.#rawData['rcs_settings.' + value.value.associated_thermostat].value.active_rcs_sensors.includes(object_key) === true;
|
|
1912
|
+
tempDevice = process_kryptonite_data(object_key, RESTTypeData);
|
|
1913
|
+
}
|
|
1914
|
+
// eslint-disable-next-line no-unused-vars
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
this?.log?.debug && this.log.debug('Error processing data for temperature sensor(s)');
|
|
1917
|
+
}
|
|
1918
|
+
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
|
|
1919
|
+
// Insert any extra options we've read in from configuration file for this device
|
|
1920
|
+
tempDevice.eveHistory =
|
|
1921
|
+
this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
|
|
1922
|
+
devices[tempDevice.serial_number] = tempDevice; // Store processed device
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
// Process data for any smoke detectors we have in the raw data
|
|
1927
|
+
const process_protect_data = (object_key, data) => {
|
|
1928
|
+
let processed = {};
|
|
1929
|
+
try {
|
|
1930
|
+
// Fix up data we need to
|
|
1931
|
+
data.serial_number = data.serial_number.toUpperCase(); // ensure serial numbers are in upper case
|
|
1932
|
+
data.excluded =
|
|
1933
|
+
typeof this.config?.devices?.[data.serial_number]?.exclude === 'boolean'
|
|
1934
|
+
? this.config.devices[data.serial_number].exclude
|
|
1935
|
+
: false; // Mark device as excluded or not
|
|
1936
|
+
data.device_type = NestAccfactory.DeviceType.SMOKESENSOR; // Nest Protect
|
|
1937
|
+
data.uuid = object_key; // Internal structure ID
|
|
1938
|
+
data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
|
|
1939
|
+
data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
|
|
1940
|
+
data.battery_level = scaleValue(data.battery_level, 0, 5400, 0, 100);
|
|
1941
|
+
data.model = 'Protect';
|
|
1942
|
+
if (data.wired_or_battery === 0) {
|
|
1943
|
+
data.model = data.model + ' (wired'; // Mains powered
|
|
1944
|
+
}
|
|
1945
|
+
if (data.wired_or_battery === 1) {
|
|
1946
|
+
data.model = data.model + ' (battery'; // Battery powered
|
|
1947
|
+
}
|
|
1948
|
+
if (data.serial_number.substring(0, 2) === '06') {
|
|
1949
|
+
data.model = data.model + ', 2nd gen)'; // Nest Protect 2nd Gen
|
|
1950
|
+
}
|
|
1951
|
+
if (data.serial_number.substring(0, 2) === '05') {
|
|
1952
|
+
data.model = data.model + ', 1st gen)'; // Nest Protect 1st Gen
|
|
1953
|
+
}
|
|
1954
|
+
let description = typeof data?.description === 'string' ? data.description : '';
|
|
1955
|
+
let location = typeof data?.location === 'string' ? data.location : '';
|
|
1956
|
+
if (description === '') {
|
|
1957
|
+
description = location;
|
|
1958
|
+
location = '';
|
|
1959
|
+
}
|
|
1960
|
+
data.description = makeHomeKitName(location === '' ? description : description + ' - ' + location);
|
|
1961
|
+
delete data.location;
|
|
1962
|
+
|
|
1963
|
+
// Insert details for when using HAP-NodeJS library rather than Homebridge
|
|
1964
|
+
if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
|
|
1965
|
+
data.hkPairingCode = this.config.options.hkPairingCode;
|
|
1966
|
+
}
|
|
1967
|
+
if (
|
|
1968
|
+
typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
|
|
1969
|
+
this.config.devices[data.serial_number].hkPairingCode !== ''
|
|
1970
|
+
) {
|
|
1971
|
+
data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
|
|
1972
|
+
}
|
|
1973
|
+
if (data?.hkPairingCode !== undefined && data?.mac_address !== undefined) {
|
|
1974
|
+
// Create mac_address in format of xx:xx:xx:xx:xx:xx
|
|
1975
|
+
data.hkUsername = data.mac_address
|
|
1976
|
+
.toString('hex')
|
|
1977
|
+
.split(/(..)/)
|
|
1978
|
+
.filter((s) => s)
|
|
1979
|
+
.join(':')
|
|
1980
|
+
.toUpperCase();
|
|
1981
|
+
}
|
|
1982
|
+
delete data.mac_address;
|
|
1983
|
+
|
|
1984
|
+
processed = data;
|
|
1985
|
+
// eslint-disable-next-line no-unused-vars
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
// Empty
|
|
1988
|
+
}
|
|
1989
|
+
return processed;
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
Object.entries(this.#rawData)
|
|
1993
|
+
.filter(
|
|
1994
|
+
([key, value]) =>
|
|
1995
|
+
(key.startsWith('topaz.') === true ||
|
|
1996
|
+
(key.startsWith('DEVICE_') === true && value.value?.device_info?.className.startsWith('topaz') === true)) &&
|
|
1997
|
+
(deviceUUID === '' || deviceUUID === key),
|
|
1998
|
+
)
|
|
1999
|
+
.forEach(([object_key, value]) => {
|
|
2000
|
+
let tempDevice = {};
|
|
2001
|
+
try {
|
|
2002
|
+
if (value.source === NestAccfactory.DataSource.PROTOBUF) {
|
|
2003
|
+
/*
|
|
2004
|
+
let RESTTypeData = {};
|
|
2005
|
+
RESTTypeData.mac_address = Buffer.from(value.value.wifi_interface.macAddress, 'base64');
|
|
2006
|
+
RESTTypeData.serial_number = value.value.device_identity.serialNumber;
|
|
2007
|
+
RESTTypeData.software_version = value.value.device_identity.softwareVersion;
|
|
2008
|
+
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
2009
|
+
RESTTypeData.line_power_present = value.value?.wall_power?.status === 'POWER_SOURCE_STATUS_ACTIVE';
|
|
2010
|
+
RESTTypeData.wired_or_battery = typeof value.value?.wall_power === 'object' ? 0 : 1;
|
|
2011
|
+
RESTTypeData.battery_level = parseFloat(value.value.battery_voltage_bank1.batteryValue.batteryVoltage.value);
|
|
2012
|
+
RESTTypeData.battery_health_state = value.value.battery_voltage_bank1.faultInformation;
|
|
2013
|
+
RESTTypeData.smoke_status = value.value.safety_alarm_smoke.alarmState === 'ALARM_STATE_ALARM' ? 2 : 0; // matches REST data
|
|
2014
|
+
RESTTypeData.co_status = value.value.safety_alarm_co.alarmState === 'ALARM_STATE_ALARM' ? 2 : 0; // matches REST data
|
|
2015
|
+
RESTTypeData.heat_status =
|
|
2016
|
+
RESTTypeData.hushed_state =
|
|
2017
|
+
value.value.safety_alarm_smoke.silenceState === 'SILENCE_STATE_SILENCED' ||
|
|
2018
|
+
value.value.safety_alarm_co.silenceState === 'SILENCE_STATE_SILENCED';
|
|
2019
|
+
RESTTypeData.ntp_green_led = value.value.night_time_promise_settings.greenLedEnabled === true;
|
|
2020
|
+
RESTTypeData.smoke_test_passed = value.value.safety_summary.warningDevices.failures.includes('FAILURE_TYPE_SMOKE') === false;
|
|
2021
|
+
RESTTypeData.heat_test_passed = value.value.safety_summary.warningDevices.failures.includes('FAILURE_TYPE_TEMP') === false;
|
|
2022
|
+
RESTTypeData.latest_alarm_test =
|
|
2023
|
+
parseInt(value.value.self_test.lastMstEnd?.second) > 0 ? parseInt(value.value.self_test.lastMstEnd.seconds) : 0;
|
|
2024
|
+
RESTTypeData.self_test_in_progress =
|
|
2025
|
+
value.value.legacy_structure_self_test.mstInProgress === true ||
|
|
2026
|
+
value.value.legacy_structure_self_test.astInProgress === true;
|
|
2027
|
+
RESTTypeData.replacement_date =
|
|
2028
|
+
value.value.legacy_protect_device_settings.replaceByDate.hasOwnProperty('seconds') === true
|
|
2029
|
+
? parseInt(value.value.legacy_protect_device_settings.replaceByDate.seconds)
|
|
2030
|
+
: 0;
|
|
2031
|
+
|
|
2032
|
+
RESTTypeData.removed_from_base =
|
|
2033
|
+
RESTTypeData.topaz_hush_key =
|
|
2034
|
+
typeof value.value.safety_structure_settings.structureHushKey === 'string'
|
|
2035
|
+
? value.value.safety_structure_settings.structureHushKey
|
|
2036
|
+
: '';
|
|
2037
|
+
RESTTypeData.detected_motion = value.value.legacy_protect_device_info.autoAway === false;
|
|
2038
|
+
RESTTypeData.description = typeof value.value?.label?.label === 'string' ? value.value.label.label : '';
|
|
2039
|
+
RESTTypeData.location = get_location_name(
|
|
2040
|
+
value.value.device_info.pairerId.resourceId,
|
|
2041
|
+
value.value.device_located_settings.whereAnnotationRid.resourceId,
|
|
2042
|
+
);
|
|
2043
|
+
tempDevice = process_protect_data(object_key, RESTTypeData);
|
|
2044
|
+
*/
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
if (value.source === NestAccfactory.DataSource.REST) {
|
|
2048
|
+
let RESTTypeData = {};
|
|
2049
|
+
RESTTypeData.mac_address = value.value.wifi_mac_address;
|
|
2050
|
+
RESTTypeData.serial_number = value.value.serial_number;
|
|
2051
|
+
RESTTypeData.software_version = value.value.software_version;
|
|
2052
|
+
RESTTypeData.online = this.#rawData['widget_track.' + value.value.thread_mac_address.toUpperCase()].value.online === true;
|
|
2053
|
+
RESTTypeData.line_power_present = value.value.line_power_present === true;
|
|
2054
|
+
RESTTypeData.wired_or_battery = value.value.wired_or_battery;
|
|
2055
|
+
RESTTypeData.battery_level = value.value.battery_level;
|
|
2056
|
+
RESTTypeData.battery_health_state = value.value.battery_health_state;
|
|
2057
|
+
RESTTypeData.smoke_status = value.value.smoke_status;
|
|
2058
|
+
RESTTypeData.co_status = value.value.co_status;
|
|
2059
|
+
RESTTypeData.heat_status = value.value.heat_status;
|
|
2060
|
+
RESTTypeData.hushed_state = value.value.hushed_state === true;
|
|
2061
|
+
RESTTypeData.ntp_green_led = value.value.ntp_green_led_enable === true;
|
|
2062
|
+
RESTTypeData.smoke_test_passed = value.value.component_smoke_test_passed === true;
|
|
2063
|
+
RESTTypeData.heat_test_passed = value.value.component_temp_test_passed === true;
|
|
2064
|
+
RESTTypeData.latest_alarm_test = value.value.latest_manual_test_end_utc_secs;
|
|
2065
|
+
RESTTypeData.self_test_in_progress =
|
|
2066
|
+
this.#rawData['safety.' + value.value.structure_id].value.manual_self_test_in_progress === true;
|
|
2067
|
+
RESTTypeData.replacement_date = value.value.replace_by_date_utc_secs;
|
|
2068
|
+
RESTTypeData.removed_from_base = value.value.removed_from_base === true;
|
|
2069
|
+
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].value.topaz_hush_key
|
|
2072
|
+
: '';
|
|
2073
|
+
RESTTypeData.detected_motion = value.value.auto_away === false;
|
|
2074
|
+
RESTTypeData.description = value.value?.description;
|
|
2075
|
+
RESTTypeData.location = get_location_name(value.value.structure_id, value.value.where_id);
|
|
2076
|
+
tempDevice = process_protect_data(object_key, RESTTypeData);
|
|
2077
|
+
}
|
|
2078
|
+
// eslint-disable-next-line no-unused-vars
|
|
2079
|
+
} catch (error) {
|
|
2080
|
+
this?.log?.debug && this.log.debug('Error processing data for smoke sensor(s)');
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
|
|
2084
|
+
// Insert any extra options we've read in from configuration file for this device
|
|
2085
|
+
tempDevice.eveHistory =
|
|
2086
|
+
this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
|
|
2087
|
+
devices[tempDevice.serial_number] = tempDevice; // Store processed device
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
// Process data for any camera/doorbell(s) we have in the raw data
|
|
2092
|
+
const process_camera_doorbell_data = (object_key, data) => {
|
|
2093
|
+
let processed = {};
|
|
2094
|
+
try {
|
|
2095
|
+
// Fix up data we need to
|
|
2096
|
+
data.serial_number = data.serial_number.toUpperCase(); // ensure serial numbers are in upper case
|
|
2097
|
+
data.excluded =
|
|
2098
|
+
typeof this.config?.devices?.[data.serial_number]?.exclude === 'boolean'
|
|
2099
|
+
? this.config.devices[data.serial_number].exclude
|
|
2100
|
+
: false; // Mark device as excluded or not
|
|
2101
|
+
data.device_type = NestAccfactory.DeviceType.CAMERA;
|
|
2102
|
+
if (data.model.toUpperCase().includes('DOORBELL') === true) {
|
|
2103
|
+
data.device_type = NestAccfactory.DeviceType.DOORBELL;
|
|
2104
|
+
}
|
|
2105
|
+
data.uuid = object_key; // Internal structure ID
|
|
2106
|
+
data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
|
|
2107
|
+
data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
|
|
2108
|
+
let description = typeof data?.description === 'string' ? data.description : '';
|
|
2109
|
+
let location = typeof data?.location === 'string' ? data.location : '';
|
|
2110
|
+
if (description === '') {
|
|
2111
|
+
description = location;
|
|
2112
|
+
location = '';
|
|
2113
|
+
}
|
|
2114
|
+
data.description = makeHomeKitName(location === '' ? description : description + ' - ' + location);
|
|
2115
|
+
delete data.location;
|
|
2116
|
+
|
|
2117
|
+
// Insert details for when using HAP-NodeJS library rather than Homebridge
|
|
2118
|
+
if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
|
|
2119
|
+
data.hkPairingCode = this.config.options.hkPairingCode;
|
|
2120
|
+
}
|
|
2121
|
+
if (
|
|
2122
|
+
typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
|
|
2123
|
+
this.config.devices[data.serial_number].hkPairingCode !== ''
|
|
2124
|
+
) {
|
|
2125
|
+
data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
|
|
2126
|
+
}
|
|
2127
|
+
if (data?.hkPairingCode !== undefined && data?.mac_address !== undefined) {
|
|
2128
|
+
// Create mac_address in format of xx:xx:xx:xx:xx:xx
|
|
2129
|
+
data.hkUsername = data.mac_address
|
|
2130
|
+
.toString('hex')
|
|
2131
|
+
.split(/(..)/)
|
|
2132
|
+
.filter((s) => s)
|
|
2133
|
+
.join(':')
|
|
2134
|
+
.toUpperCase();
|
|
2135
|
+
}
|
|
2136
|
+
delete data.mac_address;
|
|
2137
|
+
|
|
2138
|
+
processed = data;
|
|
2139
|
+
// eslint-disable-next-line no-unused-vars
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
// Empty
|
|
2142
|
+
}
|
|
2143
|
+
return processed;
|
|
2144
|
+
};
|
|
2145
|
+
|
|
2146
|
+
const PROTOBUF_CAMERA_DOORBELL_RESOURCES = [
|
|
2147
|
+
'google.resource.NeonQuartzResource',
|
|
2148
|
+
'google.resource.GreenQuartzResource',
|
|
2149
|
+
'google.resource.SpencerResource',
|
|
2150
|
+
'google.resource.VenusResource',
|
|
2151
|
+
'nest.resource.NestCamIndoorResource',
|
|
2152
|
+
'nest.resource.NestCamIQResource',
|
|
2153
|
+
'nest.resource.NestCamIQOutdoorResource',
|
|
2154
|
+
'nest.resource.NestHelloResource',
|
|
2155
|
+
'google.resource.AzizResource',
|
|
2156
|
+
'google.resource.GoogleNewmanResource',
|
|
2157
|
+
];
|
|
2158
|
+
Object.entries(this.#rawData)
|
|
2159
|
+
.filter(
|
|
2160
|
+
([key, value]) =>
|
|
2161
|
+
(key.startsWith('quartz.') === true ||
|
|
2162
|
+
(key.startsWith('DEVICE_') === true &&
|
|
2163
|
+
PROTOBUF_CAMERA_DOORBELL_RESOURCES.includes(value.value?.device_info?.typeName) === true)) &&
|
|
2164
|
+
(deviceUUID === '' || deviceUUID === key),
|
|
2165
|
+
)
|
|
2166
|
+
.forEach(([object_key, value]) => {
|
|
2167
|
+
let tempDevice = {};
|
|
2168
|
+
try {
|
|
2169
|
+
if (value.source === NestAccfactory.DataSource.PROTOBUF && value.value?.streaming_protocol !== undefined) {
|
|
2170
|
+
let RESTTypeData = {};
|
|
2171
|
+
//RESTTypeData.mac_address = value.value.wifi_interface.macAddress.toString('hex');
|
|
2172
|
+
// Use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off serial number for last 6 digits.
|
|
2173
|
+
RESTTypeData.mac_address = '18B430' + crc24(value.value.device_identity.serialNumber.toUpperCase()).toUpperCase();
|
|
2174
|
+
RESTTypeData.serial_number = value.value.device_identity.serialNumber;
|
|
2175
|
+
RESTTypeData.software_version = value.value.device_identity.softwareVersion.replace(/[^0-9.]/g, '');
|
|
2176
|
+
RESTTypeData.model = 'Camera';
|
|
2177
|
+
if (value.value.device_info.typeName === 'google.resource.NeonQuartzResource') {
|
|
2178
|
+
RESTTypeData.model = 'Cam (battery)';
|
|
2179
|
+
}
|
|
2180
|
+
if (value.value.device_info.typeName === 'google.resource.GreenQuartzResource') {
|
|
2181
|
+
RESTTypeData.model = 'Doorbell (battery)';
|
|
2182
|
+
}
|
|
2183
|
+
if (value.value.device_info.typeName === 'google.resource.SpencerResource') {
|
|
2184
|
+
RESTTypeData.model = 'Cam (wired)';
|
|
2185
|
+
}
|
|
2186
|
+
if (value.value.device_info.typeName === 'google.resource.VenusResource') {
|
|
2187
|
+
RESTTypeData.model = 'Doorbell (wired, 2nd gen)';
|
|
2188
|
+
}
|
|
2189
|
+
if (value.value.device_info.typeName === 'nest.resource.NestCamIndoorResource') {
|
|
2190
|
+
RESTTypeData.model = 'Cam Indoor (1st gen)';
|
|
2191
|
+
}
|
|
2192
|
+
if (value.value.device_info.typeName === 'nest.resource.NestCamIQResource') {
|
|
2193
|
+
RESTTypeData.model = 'Cam IQ';
|
|
2194
|
+
}
|
|
2195
|
+
if (value.value.device_info.typeName === 'nest.resource.NestCamIQOutdoorResource') {
|
|
2196
|
+
RESTTypeData.model = 'Cam Outdoor (1st gen)';
|
|
2197
|
+
}
|
|
2198
|
+
if (value.value.device_info.typeName === 'nest.resource.NestHelloResource') {
|
|
2199
|
+
RESTTypeData.model = 'Doorbell (wired, 1st gen)';
|
|
2200
|
+
}
|
|
2201
|
+
if (value.value.device_info.typeName === 'google.resource.AzizResource') {
|
|
2202
|
+
RESTTypeData.model = 'Cam with Floodlight (wired)';
|
|
2203
|
+
}
|
|
2204
|
+
if (value.value.device_info.typeName === 'google.resource.GoogleNewmanResource') {
|
|
2205
|
+
RESTTypeData.model = 'Hub Max';
|
|
2206
|
+
}
|
|
2207
|
+
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
2208
|
+
RESTTypeData.description = typeof value.value?.label?.label === 'string' ? value.value.label.label : '';
|
|
2209
|
+
RESTTypeData.location = get_location_name(
|
|
2210
|
+
value.value.device_info.pairerId.resourceId,
|
|
2211
|
+
value.value.device_located_settings.whereAnnotationRid.resourceId,
|
|
2212
|
+
);
|
|
2213
|
+
RESTTypeData.audio_enabled = value.value?.microphone_settings?.enableMicrophone === true;
|
|
2214
|
+
RESTTypeData.has_indoor_chime =
|
|
2215
|
+
value.value?.doorbell_indoor_chime_settings?.chimeType === 'CHIME_TYPE_MECHANICAL' ||
|
|
2216
|
+
value.value?.doorbell_indoor_chime_settings?.chimeType === 'CHIME_TYPE_ELECTRONIC';
|
|
2217
|
+
RESTTypeData.indoor_chime_enabled = value.value?.doorbell_indoor_chime_settings?.chimeEnabled === true;
|
|
2218
|
+
RESTTypeData.streaming_enabled = value.value?.recording_toggle?.currentCameraState === 'CAMERA_ON';
|
|
2219
|
+
//RESTTypeData.has_irled =
|
|
2220
|
+
//RESTTypeData.irled_enabled =
|
|
2221
|
+
//RESTTypeData.has_statusled =
|
|
2222
|
+
//RESTTypeData.statusled_brightness =
|
|
2223
|
+
RESTTypeData.has_microphone = value.value?.microphone_settings?.enableMicrophone === true;
|
|
2224
|
+
RESTTypeData.has_speaker = value.value?.speaker_volume?.volume === true;
|
|
2225
|
+
RESTTypeData.has_motion_detection = value.value?.observation_trigger_capabilities?.videoEventTypes?.motion?.value === true;
|
|
2226
|
+
RESTTypeData.activity_zones = [];
|
|
2227
|
+
if (value.value?.activity_zone_settings?.activityZones !== undefined) {
|
|
2228
|
+
value.value.activity_zone_settings.activityZones.forEach((zone) => {
|
|
2229
|
+
RESTTypeData.activity_zones.push({
|
|
2230
|
+
id: typeof zone.zoneProperties?.zoneId === 'number' ? zone.zoneProperties.zoneId : zone.zoneProperties.internalIndex,
|
|
2231
|
+
name: makeHomeKitName(typeof zone.zoneProperties?.name === 'string' ? zone.zoneProperties.name : ''),
|
|
2232
|
+
hidden: false,
|
|
2233
|
+
uri: '',
|
|
2234
|
+
});
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
RESTTypeData.alerts = typeof value.value?.alerts === 'object' ? value.value.alerts : [];
|
|
2238
|
+
RESTTypeData.quiet_time_enabled =
|
|
2239
|
+
parseInt(value.value?.quiet_time_settings?.quietTimeEnds?.seconds) !== 0 &&
|
|
2240
|
+
Math.floor(Date.now() / 1000) < parseInt(value.value?.quiet_time_settings?.quietTimeEnds?.second);
|
|
2241
|
+
RESTTypeData.camera_type = value.value.device_identity.vendorProductId;
|
|
2242
|
+
RESTTypeData.streaming_protocols =
|
|
2243
|
+
value.value?.streaming_protocol?.supportedProtocols !== undefined ? value.value.streaming_protocol.supportedProtocols : [];
|
|
2244
|
+
RESTTypeData.streaming_host =
|
|
2245
|
+
typeof value.value?.streaming_protocol?.directHost?.value === 'string' ? value.value.streaming_protocol.directHost.value : '';
|
|
2246
|
+
|
|
2247
|
+
tempDevice = process_camera_doorbell_data(object_key, RESTTypeData);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
if (value.source === NestAccfactory.DataSource.REST && value.value.properties['cc2migration.overview_state'] === 'NORMAL') {
|
|
2251
|
+
// We'll only use the REST API data for Camera's which have NOT been migrated to Google Home
|
|
2252
|
+
let RESTTypeData = {};
|
|
2253
|
+
RESTTypeData.mac_address = value.value.mac_address;
|
|
2254
|
+
RESTTypeData.serial_number = value.value.serial_number;
|
|
2255
|
+
RESTTypeData.software_version = value.value.software_version;
|
|
2256
|
+
RESTTypeData.model = value.value.model.replace(/nest\s*/gi, ''); // Use camera/doorbell model that Nest supplies
|
|
2257
|
+
RESTTypeData.description = value.value?.description;
|
|
2258
|
+
RESTTypeData.location = get_location_name(value.value.structure_id, value.value.where_id);
|
|
2259
|
+
RESTTypeData.streaming_enabled = value.value.streaming_state.includes('enabled') === true;
|
|
2260
|
+
RESTTypeData.nexus_api_http_server_url = value.value.nexus_api_http_server_url;
|
|
2261
|
+
RESTTypeData.online = value.value.streaming_state.includes('offline') === false;
|
|
2262
|
+
RESTTypeData.audio_enabled = value.value.audio_input_enabled === true;
|
|
2263
|
+
RESTTypeData.has_indoor_chime = value.value.capabilities.includes('indoor_chime') === true;
|
|
2264
|
+
RESTTypeData.indoor_chime_enabled = value.value.properties['doorbell.indoor_chime.enabled'] === true;
|
|
2265
|
+
RESTTypeData.has_irled = value.value.capabilities.includes('irled') === true;
|
|
2266
|
+
RESTTypeData.irled_enabled = value.value.properties['irled.state'] !== 'always_off';
|
|
2267
|
+
RESTTypeData.has_statusled = value.value.capabilities.includes('statusled') === true;
|
|
2268
|
+
RESTTypeData.has_video_flip = value.value.capabilities.includes('video.flip') === true;
|
|
2269
|
+
RESTTypeData.video_flipped = value.value.properties['video.flipped'] === true;
|
|
2270
|
+
RESTTypeData.statusled_brightness = value.value.properties['statusled.brightness'];
|
|
2271
|
+
RESTTypeData.has_microphone = value.value.capabilities.includes('audio.microphone') === true;
|
|
2272
|
+
RESTTypeData.has_speaker = value.value.capabilities.includes('audio.speaker') === true;
|
|
2273
|
+
RESTTypeData.has_motion_detection = value.value.capabilities.includes('detectors.on_camera') === true;
|
|
2274
|
+
RESTTypeData.activity_zones = value.value.activity_zones; // structure elements we added
|
|
2275
|
+
RESTTypeData.alerts = typeof value.value?.alerts === 'object' ? value.value.alerts : [];
|
|
2276
|
+
RESTTypeData.streaming_protocols = ['PROTOCOL_NEXUSTALK'];
|
|
2277
|
+
RESTTypeData.streaming_host = value.value.direct_nexustalk_host;
|
|
2278
|
+
RESTTypeData.quiet_time_enabled = false;
|
|
2279
|
+
RESTTypeData.camera_type = value.value.camera_type;
|
|
2280
|
+
|
|
2281
|
+
tempDevice = process_camera_doorbell_data(object_key, RESTTypeData);
|
|
2282
|
+
}
|
|
2283
|
+
// eslint-disable-next-line no-unused-vars
|
|
2284
|
+
} catch (error) {
|
|
2285
|
+
this?.log?.debug && this.log.debug('Error processing data for camera/doorbell(s)');
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
|
|
2289
|
+
// Insert details to allow access to camera API calls for the device
|
|
2290
|
+
if (value.connection !== undefined && typeof this.#connections?.[value.connection]?.cameraAPI === 'object') {
|
|
2291
|
+
tempDevice.apiAccess = this.#connections[value.connection].cameraAPI;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Insert any extra options we've read in from configuration file for this device
|
|
2295
|
+
tempDevice.eveHistory =
|
|
2296
|
+
this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
|
|
2297
|
+
tempDevice.hksv = this.config.options.hksv === true || this.config?.devices?.[tempDevice.serial_number]?.hksv === true;
|
|
2298
|
+
tempDevice.doorbellCooldown =
|
|
2299
|
+
typeof this.config?.devices?.[tempDevice.serial_number]?.doorbellCooldown === 'number'
|
|
2300
|
+
? this.config.devices[tempDevice.serial_number].doorbellCooldown
|
|
2301
|
+
: 60;
|
|
2302
|
+
tempDevice.motionCooldown =
|
|
2303
|
+
typeof this.config?.devices?.[tempDevice.serial_number]?.motionCooldown === 'number'
|
|
2304
|
+
? this.config.devices[tempDevice.serial_number].motionCooldown
|
|
2305
|
+
: 60;
|
|
2306
|
+
tempDevice.personCooldown =
|
|
2307
|
+
typeof this.config?.devices?.[tempDevice.serial_number]?.personCooldown === 'number'
|
|
2308
|
+
? this.config.devices[tempDevice.serial_number].personCooldown
|
|
2309
|
+
: 120;
|
|
2310
|
+
tempDevice.chimeSwitch = this.config?.devices?.[tempDevice.serial_number]?.chimeSwitch === true; // Control 'indoor' chime by switch
|
|
2311
|
+
tempDevice.localAccess = this.config?.devices?.[tempDevice.serial_number]?.localAccess === true; // Local network video streaming rather than from cloud from camera/doorbells
|
|
2312
|
+
tempDevice.ffmpeg = this.config.options.ffmpeg; // ffmpeg details, path, libraries. No ffmpeg = undefined
|
|
2313
|
+
tempDevice.maxStreams =
|
|
2314
|
+
typeof this.config.options?.maxStreams === 'number' ? this.config.options.maxStreams : this.deviceData.hksv === true ? 1 : 2;
|
|
2315
|
+
devices[tempDevice.serial_number] = tempDevice; // Store processed device
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
|
|
2319
|
+
// Process data for any structure(s) for both REST and protobuf API data
|
|
2320
|
+
// We use this to created virtual weather station(s) for each structure that has location data
|
|
2321
|
+
const process_structure_data = (object_key, data) => {
|
|
2322
|
+
let processed = {};
|
|
2323
|
+
try {
|
|
2324
|
+
// Fix up data we need to
|
|
2325
|
+
|
|
2326
|
+
// For the serial number, use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off structure for last 6 digits.
|
|
2327
|
+
data.serial_number = '18B430' + crc24(object_key).toUpperCase();
|
|
2328
|
+
data.excluded = this.config?.options?.weather === false; // Mark device as excluded or not
|
|
2329
|
+
data.device_type = NestAccfactory.DeviceType.WEATHER;
|
|
2330
|
+
data.uuid = object_key; // Internal structure ID
|
|
2331
|
+
data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
|
|
2332
|
+
data.software_version = typeof data?.software_version === 'string' ? data.software_version.replace(/-/g, '.') : '0.0.0';
|
|
2333
|
+
data.description = typeof data?.description === 'string' ? makeHomeKitName(data.description) : '';
|
|
2334
|
+
data.model = 'Weather';
|
|
2335
|
+
data.current_temperature = data.weather.current_temperature;
|
|
2336
|
+
data.current_humidity = data.weather.current_humidity;
|
|
2337
|
+
data.condition = data.weather.condition;
|
|
2338
|
+
data.wind_direction = data.weather.wind_direction;
|
|
2339
|
+
data.wind_speed = data.weather.wind_speed;
|
|
2340
|
+
data.sunrise = data.weather.sunrise;
|
|
2341
|
+
data.sunset = data.weather.sunset;
|
|
2342
|
+
data.station = data.weather.station;
|
|
2343
|
+
data.forecast = data.weather.forecast;
|
|
2344
|
+
data.elevation = 0;
|
|
2345
|
+
|
|
2346
|
+
// Either use global elevation setting or one specific for device
|
|
2347
|
+
if (typeof this.config?.devices?.[data.serial_number]?.elevation === 'number') {
|
|
2348
|
+
data.elevation = this.config.devices[data.serial_number].elevation;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
if (data.elevation === 0 && typeof this.config?.options?.elevation === 'number') {
|
|
2352
|
+
// Elevation from configuration
|
|
2353
|
+
data.elevation = this.config.options.elevation;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// Insert details for when using HAP-NodeJS library rather than Homebridge
|
|
2357
|
+
if (typeof this.config?.options?.hkPairingCode === 'string' && this.config.options.hkPairingCode !== '') {
|
|
2358
|
+
data.hkPairingCode = this.config.options.hkPairingCode;
|
|
2359
|
+
}
|
|
2360
|
+
if (
|
|
2361
|
+
typeof this.config?.devices?.[data.serial_number]?.hkPairingCode === 'string' &&
|
|
2362
|
+
this.config.devices[data.serial_number].hkPairingCode !== ''
|
|
2363
|
+
) {
|
|
2364
|
+
data.hkPairingCode = this.config.devices[data.serial_number].hkPairingCode;
|
|
2365
|
+
}
|
|
2366
|
+
if (data?.hkPairingCode !== undefined) {
|
|
2367
|
+
// Use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off serial number for last 6 digits.
|
|
2368
|
+
let tempMACAddress = '18B430' + crc24(object_key).toUpperCase();
|
|
2369
|
+
data.hkUsername = tempMACAddress
|
|
2370
|
+
.toString('hex')
|
|
2371
|
+
.split(/(..)/)
|
|
2372
|
+
.filter((s) => s)
|
|
2373
|
+
.join(':')
|
|
2374
|
+
.toUpperCase(); // Create mac_address in format of xx:xx:xx:xx:xx:xx
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
delete data.weather; // Don't need the 'weather' object in our output
|
|
2378
|
+
|
|
2379
|
+
processed = data;
|
|
2380
|
+
// eslint-disable-next-line no-unused-vars
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
// Empty
|
|
2383
|
+
}
|
|
2384
|
+
return processed;
|
|
2385
|
+
};
|
|
2386
|
+
|
|
2387
|
+
Object.entries(this.#rawData)
|
|
2388
|
+
.filter(
|
|
2389
|
+
([key]) =>
|
|
2390
|
+
(key.startsWith('structure.') === true || key.startsWith('STRUCTURE_') === true) && (deviceUUID === '' || deviceUUID === key),
|
|
2391
|
+
)
|
|
2392
|
+
.forEach(([object_key, value]) => {
|
|
2393
|
+
let tempDevice = {};
|
|
2394
|
+
try {
|
|
2395
|
+
if (value.source === NestAccfactory.DataSource.PROTOBUF) {
|
|
2396
|
+
let RESTTypeData = {};
|
|
2397
|
+
RESTTypeData.postal_code = value.value.structure_location.postalCode.value;
|
|
2398
|
+
RESTTypeData.country_code = value.value.structure_location.countryCode.value;
|
|
2399
|
+
RESTTypeData.city = typeof value.value.structure_location?.city === 'string' ? value.value.structure_location.city.value : '';
|
|
2400
|
+
RESTTypeData.state =
|
|
2401
|
+
typeof value.value.structure_location?.state === 'string' ? value.value.structure_location.state.value : '';
|
|
2402
|
+
RESTTypeData.latitude = value.value.structure_location.geoCoordinate.latitude;
|
|
2403
|
+
RESTTypeData.longitude = value.value.structure_location.geoCoordinate.longitude;
|
|
2404
|
+
RESTTypeData.description =
|
|
2405
|
+
RESTTypeData.city !== '' && RESTTypeData.state !== ''
|
|
2406
|
+
? RESTTypeData.city + ' - ' + RESTTypeData.state
|
|
2407
|
+
: value.value.structure_info.name;
|
|
2408
|
+
RESTTypeData.weather = value.value.weather;
|
|
2409
|
+
|
|
2410
|
+
// Use the REST API structure ID from the protobuf structure. This should prevent two 'weather' objects being created
|
|
2411
|
+
let tempDevice = process_structure_data(value.value.structure_info.rtsStructureId, RESTTypeData);
|
|
2412
|
+
tempDevice.uuid = object_key; // Use the protobuf structure ID post processing
|
|
2413
|
+
}
|
|
2414
|
+
if (value.source === NestAccfactory.DataSource.REST) {
|
|
2415
|
+
let RESTTypeData = {};
|
|
2416
|
+
RESTTypeData.postal_code = value.value.postal_code;
|
|
2417
|
+
RESTTypeData.country_code = value.value.country_code;
|
|
2418
|
+
RESTTypeData.city = typeof value.value?.city === 'string' ? value.value.city : '';
|
|
2419
|
+
RESTTypeData.state = typeof value.value?.state === 'string' ? value.value.state : '';
|
|
2420
|
+
RESTTypeData.latitude = value.value.latitude;
|
|
2421
|
+
RESTTypeData.longitude = value.value.longitude;
|
|
2422
|
+
RESTTypeData.description =
|
|
2423
|
+
RESTTypeData.city !== '' && RESTTypeData.state !== '' ? RESTTypeData.city + ' - ' + RESTTypeData.state : value.value.name;
|
|
2424
|
+
RESTTypeData.weather = value.value.weather;
|
|
2425
|
+
tempDevice = process_structure_data(object_key, RESTTypeData);
|
|
2426
|
+
}
|
|
2427
|
+
// eslint-disable-next-line no-unused-vars
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
this?.log?.debug && this.log.debug('Error processing data for weather');
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serial_number] === 'undefined') {
|
|
2433
|
+
// Insert any extra options we've read in from configuration file for this device
|
|
2434
|
+
tempDevice.eveHistory =
|
|
2435
|
+
this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serial_number]?.eveHistory === true;
|
|
2436
|
+
devices[tempDevice.serial_number] = tempDevice; // Store processed device
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
|
|
2440
|
+
return devices; // Return our processed data
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
async #set(deviceUUID, values) {
|
|
2444
|
+
if (
|
|
2445
|
+
typeof deviceUUID !== 'string' ||
|
|
2446
|
+
typeof this.#rawData[deviceUUID] !== 'object' ||
|
|
2447
|
+
typeof values !== 'object' ||
|
|
2448
|
+
typeof this.#connections[this.#rawData[deviceUUID]?.connection] !== 'object'
|
|
2449
|
+
) {
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
if (
|
|
2454
|
+
this.#connections[this.#rawData[deviceUUID].connection].protobufRoot !== null &&
|
|
2455
|
+
this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.PROTOBUF
|
|
2456
|
+
) {
|
|
2457
|
+
let TraitMap = this.#connections[this.#rawData[deviceUUID].connection].protobufRoot.lookup('nest.rpc.NestTraitSetRequest');
|
|
2458
|
+
let setDataToEncode = [];
|
|
2459
|
+
let protobufElement = {
|
|
2460
|
+
traitId: {
|
|
2461
|
+
resourceId: deviceUUID,
|
|
2462
|
+
traitLabel: '',
|
|
2463
|
+
},
|
|
2464
|
+
property: {
|
|
2465
|
+
type_url: '',
|
|
2466
|
+
value: {},
|
|
2467
|
+
},
|
|
2468
|
+
};
|
|
2469
|
+
|
|
2470
|
+
await Promise.all(
|
|
2471
|
+
Object.entries(values).map(async ([key, value]) => {
|
|
2472
|
+
// Reset elements at start of loop
|
|
2473
|
+
protobufElement.traitId.traitLabel = '';
|
|
2474
|
+
protobufElement.property.type_url = '';
|
|
2475
|
+
protobufElement.property.value = {};
|
|
2476
|
+
|
|
2477
|
+
if (
|
|
2478
|
+
(key === 'hvac_mode' &&
|
|
2479
|
+
typeof value === 'string' &&
|
|
2480
|
+
(value.toUpperCase() === 'OFF' ||
|
|
2481
|
+
value.toUpperCase() === 'COOL' ||
|
|
2482
|
+
value.toUpperCase() === 'HEAT' ||
|
|
2483
|
+
value.toUpperCase() === 'RANGE')) ||
|
|
2484
|
+
(key === 'target_temperature' &&
|
|
2485
|
+
this.#rawData[deviceUUID].value.eco_mode_state.ecoMode === 'ECO_MODE_INACTIVE' &&
|
|
2486
|
+
typeof value === 'number') ||
|
|
2487
|
+
(key === 'target_temperature_low' &&
|
|
2488
|
+
this.#rawData[deviceUUID].value.eco_mode_state.ecoMode === 'ECO_MODE_INACTIVE' &&
|
|
2489
|
+
typeof value === 'number') ||
|
|
2490
|
+
(key === 'target_temperature_high' &&
|
|
2491
|
+
this.#rawData[deviceUUID].value.eco_mode_state.ecoMode === 'ECO_MODE_INACTIVE' &&
|
|
2492
|
+
typeof value === 'number')
|
|
2493
|
+
) {
|
|
2494
|
+
// Set either the 'mode' and/or non-eco temperatures on the target thermostat
|
|
2495
|
+
let coolingTarget = this.#rawData[deviceUUID].value.target_temperature_settings.targetTemperature.coolingTarget.value;
|
|
2496
|
+
let heatingTarget = this.#rawData[deviceUUID].value.target_temperature_settings.targetTemperature.heatingTarget.value;
|
|
2497
|
+
|
|
2498
|
+
if (
|
|
2499
|
+
key === 'target_temperature_low' ||
|
|
2500
|
+
(key === 'target_temperature' &&
|
|
2501
|
+
this.#rawData[deviceUUID].value.target_temperature_settings.targetTemperature.setpointType === 'SET_POINT_TYPE_HEAT')
|
|
2502
|
+
) {
|
|
2503
|
+
heatingTarget = value;
|
|
2504
|
+
}
|
|
2505
|
+
if (
|
|
2506
|
+
key === 'target_temperature_high' ||
|
|
2507
|
+
(key === 'target_temperature' &&
|
|
2508
|
+
this.#rawData[deviceUUID].value.target_temperature_settings.targetTemperature.setpointType === 'SET_POINT_TYPE_COOL')
|
|
2509
|
+
) {
|
|
2510
|
+
coolingTarget = value;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
protobufElement.traitId.traitLabel = 'target_temperature_settings';
|
|
2514
|
+
protobufElement.property.type_url = 'type.nestlabs.com/nest.trait.hvac.TargetTemperatureSettingsTrait';
|
|
2515
|
+
// eslint-disable-next-line no-undef
|
|
2516
|
+
protobufElement.property.value.targetTemperature = structuredClone(this.#rawData[deviceUUID].value.target_temperature_settings);
|
|
2517
|
+
protobufElement.property.value.targetTemperature.setpointType =
|
|
2518
|
+
key === 'hvac_mode' && value.toUpperCase() !== 'OFF'
|
|
2519
|
+
? 'SET_POINT_TYPE_' + value.toUpperCase()
|
|
2520
|
+
: this.#rawData[deviceUUID].value.target_temperature_settings.targetTemperature.setpointType;
|
|
2521
|
+
protobufElement.property.value.targetTemperature.heatingTarget = { value: heatingTarget };
|
|
2522
|
+
protobufElement.property.value.targetTemperature.coolingTarget = { value: coolingTarget };
|
|
2523
|
+
protobufElement.property.value.targetTemperature.currentActorInfo = {
|
|
2524
|
+
method: 'HVAC_ACTOR_METHOD_IOS',
|
|
2525
|
+
originator: {
|
|
2526
|
+
resourceId: Object.keys(this.#rawData)
|
|
2527
|
+
.filter((key) => key.includes('USER_'))
|
|
2528
|
+
.toString(),
|
|
2529
|
+
},
|
|
2530
|
+
timeOfAction: { seconds: Math.floor(Date.now() / 1000), nanos: (Date.now() % 1000) * 1e6 },
|
|
2531
|
+
originatorRtsId: '',
|
|
2532
|
+
};
|
|
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
|
+
}
|
|
2546
|
+
|
|
2547
|
+
if (
|
|
2548
|
+
(key === 'target_temperature' &&
|
|
2549
|
+
this.#rawData[deviceUUID].value.eco_mode_state.ecoMode !== 'ECO_MODE_INACTIVE' &&
|
|
2550
|
+
typeof value === 'number') ||
|
|
2551
|
+
(key === 'target_temperature_low' &&
|
|
2552
|
+
this.#rawData[deviceUUID].value.eco_mode_state.ecoMode !== 'ECO_MODE_INACTIVE' &&
|
|
2553
|
+
typeof value === 'number') ||
|
|
2554
|
+
(key === 'target_temperature_high' &&
|
|
2555
|
+
this.#rawData[deviceUUID].value.eco_mode_state.ecoMode !== 'ECO_MODE_INACTIVE' &&
|
|
2556
|
+
typeof value === 'number')
|
|
2557
|
+
) {
|
|
2558
|
+
// Set eco mode temperatures on the target thermostat
|
|
2559
|
+
protobufElement.traitId.traitLabel = 'eco_mode_settings';
|
|
2560
|
+
protobufElement.property.type_url = 'type.nestlabs.com/nest.trait.hvac.EcoModeSettingsTrait';
|
|
2561
|
+
// eslint-disable-next-line no-undef
|
|
2562
|
+
protobufElement.property.value = structuredClone(this.#rawData[deviceUUID].value.eco_mode_settings);
|
|
2563
|
+
protobufElement.property.value.ecoTemperatureHeat.value.value =
|
|
2564
|
+
protobufElement.property.value.ecoTemperatureHeat.enabled === true &&
|
|
2565
|
+
protobufElement.property.value.ecoTemperatureCool.enabled === false
|
|
2566
|
+
? value
|
|
2567
|
+
: protobufElement.property.value.ecoTemperatureHeat.value.value;
|
|
2568
|
+
protobufElement.property.value.ecoTemperatureCool.value.value =
|
|
2569
|
+
protobufElement.property.value.ecoTemperatureHeat.enabled === false &&
|
|
2570
|
+
protobufElement.property.value.ecoTemperatureCool.enabled === true
|
|
2571
|
+
? value
|
|
2572
|
+
: protobufElement.property.value.ecoTemperatureCool.value.value;
|
|
2573
|
+
protobufElement.property.value.ecoTemperatureHeat.value.value =
|
|
2574
|
+
protobufElement.property.value.ecoTemperatureHeat.enabled === true &&
|
|
2575
|
+
protobufElement.property.value.ecoTemperatureCool.enabled === true &&
|
|
2576
|
+
key === 'target_temperature_low'
|
|
2577
|
+
? value
|
|
2578
|
+
: protobufElement.property.value.ecoTemperatureHeat.value.value;
|
|
2579
|
+
protobufElement.property.value.ecoTemperatureCool.value.value =
|
|
2580
|
+
protobufElement.property.value.ecoTemperatureHeat.enabled === true &&
|
|
2581
|
+
protobufElement.property.value.ecoTemperatureCool.enabled === true &&
|
|
2582
|
+
key === 'target_temperature_high'
|
|
2583
|
+
? value
|
|
2584
|
+
: protobufElement.property.value.ecoTemperatureCool.value.value;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
if (key === 'temperature_scale' && typeof value === 'string' && (value.toUpperCase() === 'C' || value.toUpperCase() === 'F')) {
|
|
2588
|
+
// Set the temperature scale on the target thermostat
|
|
2589
|
+
protobufElement.traitId.traitLabel = 'display_settings';
|
|
2590
|
+
protobufElement.property.type_url = 'type.nestlabs.com/nest.trait.hvac.DisplaySettingsTrait';
|
|
2591
|
+
// eslint-disable-next-line no-undef
|
|
2592
|
+
protobufElement.property.value = structuredClone(this.#rawData[deviceUUID].value.display_settings);
|
|
2593
|
+
protobufElement.property.value.temperatureScale = value.toUpperCase() === 'F' ? 'TEMPERATURE_SCALE_F' : 'TEMPERATURE_SCALE_C';
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
if (key === 'temperature_lock' && typeof value === 'boolean') {
|
|
2597
|
+
// Set lock mode on the target thermostat
|
|
2598
|
+
protobufElement.traitId.traitLabel = 'temperature_lock_settings';
|
|
2599
|
+
protobufElement.property.type_url = 'type.nestlabs.com/nest.trait.hvac.TemperatureLockSettingsTrait';
|
|
2600
|
+
// eslint-disable-next-line no-undef
|
|
2601
|
+
protobufElement.property.value = structuredClone(this.#rawData[deviceUUID].value.temperature_lock_settings);
|
|
2602
|
+
protobufElement.property.value.enabled = value === true;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
if (key === 'fan_state' && typeof value === 'boolean') {
|
|
2606
|
+
// Set fan mode on the target thermostat
|
|
2607
|
+
let endTime =
|
|
2608
|
+
value === true
|
|
2609
|
+
? Math.floor(Date.now() / 1000) + this.#rawData[deviceUUID].value.fan_control_settings.timerDuration.seconds
|
|
2610
|
+
: 0;
|
|
2611
|
+
|
|
2612
|
+
protobufElement.traitId.traitLabel = 'fan_control_settings';
|
|
2613
|
+
protobufElement.property.type_url = 'type.nestlabs.com/nest.trait.hvac.FanControlSettingsTrait';
|
|
2614
|
+
// eslint-disable-next-line no-undef
|
|
2615
|
+
protobufElement.property.value = structuredClone(this.#rawData[deviceUUID].value.fan_control_settings);
|
|
2616
|
+
protobufElement.property.value.timerEnd = { seconds: endTime, nanos: (endTime % 1000) * 1e6 };
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
//if (key === 'statusled.brightness'
|
|
2620
|
+
//if (key === 'irled.state'
|
|
2621
|
+
|
|
2622
|
+
if (key === 'streaming.enabled' && typeof value === 'boolean') {
|
|
2623
|
+
// Turn camera video on/off
|
|
2624
|
+
protobufElement.traitId.traitLabel = 'recording_toggle_settings';
|
|
2625
|
+
protobufElement.property.type_url = 'type.nestlabs.com/nest.trait.product.camera.RecordingToggleSettingsTrait';
|
|
2626
|
+
// eslint-disable-next-line no-undef
|
|
2627
|
+
protobufElement.property.value = structuredClone(this.#rawData[deviceUUID].value.recording_toggle_settings);
|
|
2628
|
+
protobufElement.property.value.targetCameraState = value === true ? 'CAMERA_ON' : 'CAMERA_OFF';
|
|
2629
|
+
protobufElement.property.value.changeModeReason = 2;
|
|
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?
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
if (key === 'audio.enabled' && typeof value === 'boolean') {
|
|
2638
|
+
// Enable/disable microphone on camera/doorbell
|
|
2639
|
+
protobufElement.traitId.traitLabel = 'microphone_settings';
|
|
2640
|
+
protobufElement.property.type_url = 'type.nestlabs.com/nest.trait.audio.MicrophoneSettingsTrait';
|
|
2641
|
+
// eslint-disable-next-line no-undef
|
|
2642
|
+
protobufElement.property.value = structuredClone(this.#rawData[deviceUUID].value.microphone_settings);
|
|
2643
|
+
protobufElement.property.value.enableMicrophone = value;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
if (key === 'doorbell.indoor_chime.enabled' && typeof value === 'boolean') {
|
|
2647
|
+
// Enable/disable chime status on doorbell
|
|
2648
|
+
protobufElement.traitId.traitLabel = 'doorbell_indoor_chime_settings';
|
|
2649
|
+
protobufElement.property.type_url = 'type.nestlabs.com/nest.trait.product.doorbell.DoorbellIndoorChimeSettingsTrait';
|
|
2650
|
+
// eslint-disable-next-line no-undef
|
|
2651
|
+
protobufElement.property.value = structuredClone(this.#rawData[deviceUUID].value.doorbell_indoor_chime_settings);
|
|
2652
|
+
protobufElement.property.value.chimeEnabled = value;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
if (protobufElement.traitId.traitLabel === '' || protobufElement.property.type_url === '') {
|
|
2656
|
+
this?.log?.debug && this.log.debug('Unknown protobuf set key for device', deviceUUID, key, value);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
if (protobufElement.traitId.traitLabel !== '' && protobufElement.property.type_url !== '') {
|
|
2660
|
+
let trait = this.#connections[this.#rawData[deviceUUID].connection].protobufRoot.lookup(
|
|
2661
|
+
protobufElement.property.type_url.split('/')[1],
|
|
2662
|
+
);
|
|
2663
|
+
protobufElement.property.value = trait.encode(trait.fromObject(protobufElement.property.value)).finish();
|
|
2664
|
+
// eslint-disable-next-line no-undef
|
|
2665
|
+
setDataToEncode.push(structuredClone(protobufElement));
|
|
2666
|
+
}
|
|
2667
|
+
}),
|
|
2668
|
+
);
|
|
2669
|
+
|
|
2670
|
+
if (setDataToEncode.length !== 0 && TraitMap !== null) {
|
|
2671
|
+
let encodedData = TraitMap.encode(TraitMap.fromObject({ set: setDataToEncode })).finish();
|
|
2672
|
+
let request = {
|
|
2673
|
+
method: 'post',
|
|
2674
|
+
url:
|
|
2675
|
+
'https://' +
|
|
2676
|
+
this.#connections[this.#rawData[deviceUUID].connection].protobufAPIHost +
|
|
2677
|
+
'/nestlabs.gateway.v1.TraitBatchApi/BatchUpdateState',
|
|
2678
|
+
headers: {
|
|
2679
|
+
'User-Agent': USERAGENT,
|
|
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
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
if (this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.REST && deviceUUID.startsWith('quartz.') === true) {
|
|
2701
|
+
// Set value on Nest Camera/Doorbell
|
|
2702
|
+
await Promise.all(
|
|
2703
|
+
Object.entries(values).map(async ([key, value]) => {
|
|
2704
|
+
let request = {
|
|
2705
|
+
method: 'post',
|
|
2706
|
+
url: 'https://webapi.' + this.#connections[this.#rawData[deviceUUID].connection].cameraAPIHost + '/api/dropcams.set_properties',
|
|
2707
|
+
headers: {
|
|
2708
|
+
referer: 'https://' + this.#connections[this.#rawData[deviceUUID].connection].referer,
|
|
2709
|
+
'User-Agent': USERAGENT,
|
|
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],
|
|
2718
|
+
};
|
|
2719
|
+
await axios(request)
|
|
2720
|
+
.then((response) => {
|
|
2721
|
+
if (
|
|
2722
|
+
typeof response.status !== 'number' ||
|
|
2723
|
+
response.status !== 200 ||
|
|
2724
|
+
typeof response.data.status !== 'number' ||
|
|
2725
|
+
response.data.status !== 0
|
|
2726
|
+
) {
|
|
2727
|
+
throw new Error('REST API camera update for failed with error');
|
|
2728
|
+
}
|
|
2729
|
+
})
|
|
2730
|
+
.catch((error) => {
|
|
2731
|
+
this?.log?.debug &&
|
|
2732
|
+
this.log.debug('REST API camera update for failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2733
|
+
});
|
|
2734
|
+
}),
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
if (this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.REST && deviceUUID.startsWith('quartz.') === false) {
|
|
2739
|
+
// set values on other Nest devices besides cameras/doorbells
|
|
2740
|
+
await Promise.all(
|
|
2741
|
+
Object.entries(values).map(async ([key, value]) => {
|
|
2742
|
+
let restAPIJSONData = { objects: [] };
|
|
2743
|
+
|
|
2744
|
+
if (deviceUUID.startsWith('device.') === false) {
|
|
2745
|
+
restAPIJSONData.objects.push({ object_key: deviceUUID, op: 'MERGE', value: { [key]: value } });
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
// Some elements when setting thermostat data are located in a different object locations than with the device object
|
|
2749
|
+
// Handle this scenario below
|
|
2750
|
+
if (deviceUUID.startsWith('device.') === true) {
|
|
2751
|
+
let RESTStructureUUID = deviceUUID;
|
|
2752
|
+
|
|
2753
|
+
if (
|
|
2754
|
+
(key === 'hvac_mode' &&
|
|
2755
|
+
typeof value === 'string' &&
|
|
2756
|
+
(value.toUpperCase() === 'OFF' ||
|
|
2757
|
+
value.toUpperCase() === 'COOL' ||
|
|
2758
|
+
value.toUpperCase() === 'HEAT' ||
|
|
2759
|
+
value.toUpperCase() === 'RANGE')) ||
|
|
2760
|
+
(key === 'target_temperature' && typeof value === 'number') ||
|
|
2761
|
+
(key === 'target_temperature_low' && typeof value === 'number') ||
|
|
2762
|
+
(key === 'target_temperature_high' && typeof value === 'number')
|
|
2763
|
+
) {
|
|
2764
|
+
RESTStructureUUID = 'shared.' + deviceUUID.split('.')[1];
|
|
2765
|
+
}
|
|
2766
|
+
restAPIJSONData.objects.push({ object_key: RESTStructureUUID, op: 'MERGE', value: { [key]: value } });
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
if (restAPIJSONData.objects.length !== 0) {
|
|
2770
|
+
let request = {
|
|
2771
|
+
method: 'post',
|
|
2772
|
+
url: this.#connections[this.#rawData[deviceUUID].connection].transport_url + '/v5/put',
|
|
2773
|
+
responseType: 'json',
|
|
2774
|
+
headers: {
|
|
2775
|
+
'User-Agent': USERAGENT,
|
|
2776
|
+
Authorization: 'Basic ' + this.#connections[this.#rawData[deviceUUID].connection].token,
|
|
2777
|
+
},
|
|
2778
|
+
data: JSON.stringify(restAPIJSONData),
|
|
2779
|
+
};
|
|
2780
|
+
await axios(request)
|
|
2781
|
+
.then(async (response) => {
|
|
2782
|
+
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
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
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}),
|
|
2792
|
+
);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
async #get(deviceUUID, values) {
|
|
2797
|
+
if (
|
|
2798
|
+
typeof deviceUUID !== 'string' ||
|
|
2799
|
+
typeof this.#rawData[deviceUUID] !== 'object' ||
|
|
2800
|
+
typeof values !== 'object' ||
|
|
2801
|
+
typeof this.#connections[this.#rawData[deviceUUID]?.connection] !== 'object'
|
|
2802
|
+
) {
|
|
2803
|
+
values = {};
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
await Promise.all(
|
|
2807
|
+
Object.entries(values).map(async ([key]) => {
|
|
2808
|
+
// We'll return the data under the original key value
|
|
2809
|
+
// By default, the returned value will be undefined. If call is successful, the key value will have the data requested
|
|
2810
|
+
values[key] = undefined;
|
|
2811
|
+
|
|
2812
|
+
if (
|
|
2813
|
+
this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.REST &&
|
|
2814
|
+
key === 'camera_snapshot' &&
|
|
2815
|
+
deviceUUID.startsWith('quartz.') === true
|
|
2816
|
+
) {
|
|
2817
|
+
// Attempt to retrieve snapshot from camera via REST API
|
|
2818
|
+
let request = {
|
|
2819
|
+
method: 'get',
|
|
2820
|
+
url: this.#rawData[deviceUUID].value.nexus_api_http_server_url + '/get_image?uuid=' + deviceUUID.split('.')[1],
|
|
2821
|
+
headers: {
|
|
2822
|
+
referer: 'https://' + this.#connections[this.#rawData[deviceUUID].connection].referer,
|
|
2823
|
+
'User-Agent': USERAGENT,
|
|
2824
|
+
accept: '*/*',
|
|
2825
|
+
[this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.key]:
|
|
2826
|
+
this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.value +
|
|
2827
|
+
this.#connections[this.#rawData[deviceUUID].connection].cameraAPI.token,
|
|
2828
|
+
},
|
|
2829
|
+
responseType: 'arraybuffer',
|
|
2830
|
+
timeout: 3000,
|
|
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;
|
|
2850
|
+
})
|
|
2851
|
+
.catch((error) => {
|
|
2852
|
+
this?.log?.debug &&
|
|
2853
|
+
this.log.debug('REST API camera snapshot failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
if (
|
|
2858
|
+
this.#rawData[deviceUUID]?.source === NestAccfactory.DataSource.PROTOBUF &&
|
|
2859
|
+
this.#connections[this.#rawData[deviceUUID].connection].protobufRoot !== null &&
|
|
2860
|
+
this.#rawData[deviceUUID]?.value?.device_identity?.vendorProductId !== undefined &&
|
|
2861
|
+
key === 'camera_snapshot'
|
|
2862
|
+
) {
|
|
2863
|
+
// Attempt to retrieve snapshot from camera via protobuf API
|
|
2864
|
+
// First, request to get snapshot url image updated
|
|
2865
|
+
let commandResponse = await this.#protobufCommand(deviceUUID, [
|
|
2866
|
+
{
|
|
2867
|
+
traitLabel: 'upload_live_image',
|
|
2868
|
+
command: {
|
|
2869
|
+
type_url: 'type.nestlabs.com/nest.trait.product.camera.UploadLiveImageTrait.UploadLiveImageRequest',
|
|
2870
|
+
value: {},
|
|
2871
|
+
},
|
|
2872
|
+
},
|
|
2873
|
+
]);
|
|
2874
|
+
|
|
2875
|
+
if (commandResponse?.resourceCommandResponse?.[0]?.traitOperations?.[0]?.progress === 'COMPLETE') {
|
|
2876
|
+
// Snapshot url image has beeen updated, so no retrieve it
|
|
2877
|
+
let request = {
|
|
2878
|
+
method: 'get',
|
|
2879
|
+
url: this.#rawData[deviceUUID].value.upload_live_image.liveImageUrl,
|
|
2880
|
+
headers: {
|
|
2881
|
+
'User-Agent': USERAGENT,
|
|
2882
|
+
accept: '*/*',
|
|
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,
|
|
2886
|
+
},
|
|
2887
|
+
responseType: 'arraybuffer',
|
|
2888
|
+
timeout: 3000,
|
|
2889
|
+
};
|
|
2890
|
+
await axios(request)
|
|
2891
|
+
.then((response) => {
|
|
2892
|
+
if (typeof response.status !== 'number' || response.status !== 200) {
|
|
2893
|
+
throw new Error('protobuf API camera snapshot failed with error');
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
values[key] = response.data;
|
|
2897
|
+
})
|
|
2898
|
+
.catch((error) => {
|
|
2899
|
+
this?.log?.debug &&
|
|
2900
|
+
this.log.debug('protobuf API camera snapshot failed with error for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
}),
|
|
2905
|
+
);
|
|
2906
|
+
|
|
2907
|
+
// Send results back via event
|
|
2908
|
+
this.#eventEmitter.emit(HomeKitDevice.GET + '->' + deviceUUID, values);
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
async #getWeatherData(connectionType, deviceUUID, latitude, longitude) {
|
|
2912
|
+
let weatherData = {};
|
|
2913
|
+
if (typeof this.#rawData[deviceUUID]?.value?.weather === 'object') {
|
|
2914
|
+
weatherData = this.#rawData[deviceUUID].value.weather;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
let request = {
|
|
2918
|
+
method: 'get',
|
|
2919
|
+
url: this.#connections[connectionType].weather_url + latitude + ',' + longitude,
|
|
2920
|
+
headers: {
|
|
2921
|
+
'User-Agent': USERAGENT,
|
|
2922
|
+
},
|
|
2923
|
+
responseType: 'json',
|
|
2924
|
+
timeout: NESTAPITIMEOUT,
|
|
2925
|
+
};
|
|
2926
|
+
await axios(request)
|
|
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') {
|
|
2933
|
+
// Store the lat/long details in the weather data object
|
|
2934
|
+
weatherData.latitude = latitude;
|
|
2935
|
+
weatherData.longitude = longitude;
|
|
2936
|
+
|
|
2937
|
+
// Update weather data object
|
|
2938
|
+
weatherData.current_temperature = adjustTemperature(response.data[latitude + ',' + longitude].current.temp_c, 'C', 'C', false);
|
|
2939
|
+
weatherData.current_humidity = response.data[latitude + ',' + longitude].current.humidity;
|
|
2940
|
+
weatherData.condition = response.data[latitude + ',' + longitude].current.condition;
|
|
2941
|
+
weatherData.wind_direction = response.data[latitude + ',' + longitude].current.wind_dir;
|
|
2942
|
+
weatherData.wind_speed = response.data[latitude + ',' + longitude].current.wind_mph * 1.609344; // convert to km/h
|
|
2943
|
+
weatherData.sunrise = response.data[latitude + ',' + longitude].current.sunrise;
|
|
2944
|
+
weatherData.sunset = response.data[latitude + ',' + longitude].current.sunset;
|
|
2945
|
+
weatherData.station = response.data[latitude + ',' + longitude].location.short_name;
|
|
2946
|
+
weatherData.forecast = response.data[latitude + ',' + longitude].forecast.daily[0].condition;
|
|
2947
|
+
}
|
|
2948
|
+
})
|
|
2949
|
+
.catch((error) => {
|
|
2950
|
+
this?.log?.debug &&
|
|
2951
|
+
this.log.debug('REST API failed to retireve weather details for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
2952
|
+
});
|
|
2953
|
+
return weatherData;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
async #protobufCommand(deviceUUID, commands) {
|
|
2957
|
+
if (
|
|
2958
|
+
typeof deviceUUID !== 'string' ||
|
|
2959
|
+
typeof this.#rawData?.[deviceUUID] !== 'object' ||
|
|
2960
|
+
this.#rawData[deviceUUID]?.source !== NestAccfactory.DataSource.PROTOBUF ||
|
|
2961
|
+
Array.isArray(commands === false) ||
|
|
2962
|
+
typeof this.#connections[this.#rawData[deviceUUID]?.connection] !== 'object'
|
|
2963
|
+
) {
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
let commandResponse = undefined;
|
|
2968
|
+
let encodedData = undefined;
|
|
2969
|
+
|
|
2970
|
+
// Build the protobuf command object for encoding
|
|
2971
|
+
let protobufElement = {
|
|
2972
|
+
resourceRequest: {
|
|
2973
|
+
resourceId: deviceUUID,
|
|
2974
|
+
requestId: crypto.randomUUID(),
|
|
2975
|
+
},
|
|
2976
|
+
resourceCommands: commands,
|
|
2977
|
+
};
|
|
2978
|
+
|
|
2979
|
+
// End code each of the commands
|
|
2980
|
+
protobufElement.resourceCommands.forEach((command) => {
|
|
2981
|
+
let trait = this.#connections[this.#rawData[deviceUUID].connection].protobufRoot.lookup(command.command.type_url.split('/')[1]);
|
|
2982
|
+
if (trait !== null) {
|
|
2983
|
+
command.command.value = trait.encode(trait.fromObject(command.command.value)).finish();
|
|
2984
|
+
}
|
|
2985
|
+
});
|
|
2986
|
+
|
|
2987
|
+
let TraitMap = this.#connections[this.#rawData[deviceUUID].connection].protobufRoot.lookup(
|
|
2988
|
+
'nestlabs.gateway.v1.ResourceCommandRequest',
|
|
2989
|
+
);
|
|
2990
|
+
if (TraitMap !== null) {
|
|
2991
|
+
encodedData = TraitMap.encode(TraitMap.fromObject(protobufElement)).finish();
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
if (encodedData !== undefined) {
|
|
2995
|
+
let request = {
|
|
2996
|
+
method: 'post',
|
|
2997
|
+
url:
|
|
2998
|
+
'https://' +
|
|
2999
|
+
this.#connections[this.#rawData[deviceUUID].connection].protobufAPIHost +
|
|
3000
|
+
'/nestlabs.gateway.v1.ResourceApi/SendCommand',
|
|
3001
|
+
headers: {
|
|
3002
|
+
'User-Agent': USERAGENT,
|
|
3003
|
+
Authorization: 'Basic ' + this.#connections[this.#rawData[deviceUUID].connection].token,
|
|
3004
|
+
'Content-Type': 'application/x-protobuf',
|
|
3005
|
+
'X-Accept-Content-Transfer-Encoding': 'binary',
|
|
3006
|
+
'X-Accept-Response-Streaming': 'true',
|
|
3007
|
+
},
|
|
3008
|
+
responseType: 'arraybuffer',
|
|
3009
|
+
data: encodedData,
|
|
3010
|
+
};
|
|
3011
|
+
|
|
3012
|
+
await axios(request)
|
|
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();
|
|
3022
|
+
})
|
|
3023
|
+
.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);
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
return commandResponse;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
// General helper functions which don't need to be part of an object class
|
|
3034
|
+
function adjustTemperature(temperature, currentTemperatureUnit, targetTemperatureUnit, round) {
|
|
3035
|
+
// Converts temperatures between C/F and vice-versa
|
|
3036
|
+
// Also rounds temperatures to 0.5 increments for C and 1.0 for F
|
|
3037
|
+
if (targetTemperatureUnit.toUpperCase() === 'C') {
|
|
3038
|
+
if (currentTemperatureUnit.toUpperCase() === 'F') {
|
|
3039
|
+
// convert from F to C
|
|
3040
|
+
temperature = ((temperature - 32) * 5) / 9;
|
|
3041
|
+
}
|
|
3042
|
+
if (round === true) {
|
|
3043
|
+
// round to nearest 0.5C
|
|
3044
|
+
temperature = Math.round(temperature * 2) * 0.5;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
if (targetTemperatureUnit.toUpperCase() === 'F') {
|
|
3049
|
+
if (currentTemperatureUnit.toUpperCase() === 'C') {
|
|
3050
|
+
// convert from C to F
|
|
3051
|
+
temperature = (temperature * 9) / 5 + 32;
|
|
3052
|
+
}
|
|
3053
|
+
if (round === true) {
|
|
3054
|
+
// round to nearest 1F
|
|
3055
|
+
temperature = Math.round(temperature);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
return temperature;
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function makeHomeKitName(nameToMakeValid) {
|
|
3063
|
+
// Strip invalid characters to meet HomeKit naming requirements
|
|
3064
|
+
// Ensure only letters or numbers are at the beginning AND/OR end of string
|
|
3065
|
+
// Matches against uni-code characters
|
|
3066
|
+
return nameToMakeValid
|
|
3067
|
+
.replace(/[^\p{L}\p{N}\p{Z}\u2019.,-]/gu, '')
|
|
3068
|
+
.replace(/^[^\p{L}\p{N}]*/gu, '')
|
|
3069
|
+
.replace(/[^\p{L}\p{N}]+$/gu, '');
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
function crc24(valueToHash) {
|
|
3073
|
+
const crc24HashTable = [
|
|
3074
|
+
0x000000, 0x864cfb, 0x8ad50d, 0x0c99f6, 0x93e6e1, 0x15aa1a, 0x1933ec, 0x9f7f17, 0xa18139, 0x27cdc2, 0x2b5434, 0xad18cf, 0x3267d8,
|
|
3075
|
+
0xb42b23, 0xb8b2d5, 0x3efe2e, 0xc54e89, 0x430272, 0x4f9b84, 0xc9d77f, 0x56a868, 0xd0e493, 0xdc7d65, 0x5a319e, 0x64cfb0, 0xe2834b,
|
|
3076
|
+
0xee1abd, 0x685646, 0xf72951, 0x7165aa, 0x7dfc5c, 0xfbb0a7, 0x0cd1e9, 0x8a9d12, 0x8604e4, 0x00481f, 0x9f3708, 0x197bf3, 0x15e205,
|
|
3077
|
+
0x93aefe, 0xad50d0, 0x2b1c2b, 0x2785dd, 0xa1c926, 0x3eb631, 0xb8faca, 0xb4633c, 0x322fc7, 0xc99f60, 0x4fd39b, 0x434a6d, 0xc50696,
|
|
3078
|
+
0x5a7981, 0xdc357a, 0xd0ac8c, 0x56e077, 0x681e59, 0xee52a2, 0xe2cb54, 0x6487af, 0xfbf8b8, 0x7db443, 0x712db5, 0xf7614e, 0x19a3d2,
|
|
3079
|
+
0x9fef29, 0x9376df, 0x153a24, 0x8a4533, 0x0c09c8, 0x00903e, 0x86dcc5, 0xb822eb, 0x3e6e10, 0x32f7e6, 0xb4bb1d, 0x2bc40a, 0xad88f1,
|
|
3080
|
+
0xa11107, 0x275dfc, 0xdced5b, 0x5aa1a0, 0x563856, 0xd074ad, 0x4f0bba, 0xc94741, 0xc5deb7, 0x43924c, 0x7d6c62, 0xfb2099, 0xf7b96f,
|
|
3081
|
+
0x71f594, 0xee8a83, 0x68c678, 0x645f8e, 0xe21375, 0x15723b, 0x933ec0, 0x9fa736, 0x19ebcd, 0x8694da, 0x00d821, 0x0c41d7, 0x8a0d2c,
|
|
3082
|
+
0xb4f302, 0x32bff9, 0x3e260f, 0xb86af4, 0x2715e3, 0xa15918, 0xadc0ee, 0x2b8c15, 0xd03cb2, 0x567049, 0x5ae9bf, 0xdca544, 0x43da53,
|
|
3083
|
+
0xc596a8, 0xc90f5e, 0x4f43a5, 0x71bd8b, 0xf7f170, 0xfb6886, 0x7d247d, 0xe25b6a, 0x641791, 0x688e67, 0xeec29c, 0x3347a4, 0xb50b5f,
|
|
3084
|
+
0xb992a9, 0x3fde52, 0xa0a145, 0x26edbe, 0x2a7448, 0xac38b3, 0x92c69d, 0x148a66, 0x181390, 0x9e5f6b, 0x01207c, 0x876c87, 0x8bf571,
|
|
3085
|
+
0x0db98a, 0xf6092d, 0x7045d6, 0x7cdc20, 0xfa90db, 0x65efcc, 0xe3a337, 0xef3ac1, 0x69763a, 0x578814, 0xd1c4ef, 0xdd5d19, 0x5b11e2,
|
|
3086
|
+
0xc46ef5, 0x42220e, 0x4ebbf8, 0xc8f703, 0x3f964d, 0xb9dab6, 0xb54340, 0x330fbb, 0xac70ac, 0x2a3c57, 0x26a5a1, 0xa0e95a, 0x9e1774,
|
|
3087
|
+
0x185b8f, 0x14c279, 0x928e82, 0x0df195, 0x8bbd6e, 0x872498, 0x016863, 0xfad8c4, 0x7c943f, 0x700dc9, 0xf64132, 0x693e25, 0xef72de,
|
|
3088
|
+
0xe3eb28, 0x65a7d3, 0x5b59fd, 0xdd1506, 0xd18cf0, 0x57c00b, 0xc8bf1c, 0x4ef3e7, 0x426a11, 0xc426ea, 0x2ae476, 0xaca88d, 0xa0317b,
|
|
3089
|
+
0x267d80, 0xb90297, 0x3f4e6c, 0x33d79a, 0xb59b61, 0x8b654f, 0x0d29b4, 0x01b042, 0x87fcb9, 0x1883ae, 0x9ecf55, 0x9256a3, 0x141a58,
|
|
3090
|
+
0xefaaff, 0x69e604, 0x657ff2, 0xe33309, 0x7c4c1e, 0xfa00e5, 0xf69913, 0x70d5e8, 0x4e2bc6, 0xc8673d, 0xc4fecb, 0x42b230, 0xddcd27,
|
|
3091
|
+
0x5b81dc, 0x57182a, 0xd154d1, 0x26359f, 0xa07964, 0xace092, 0x2aac69, 0xb5d37e, 0x339f85, 0x3f0673, 0xb94a88, 0x87b4a6, 0x01f85d,
|
|
3092
|
+
0x0d61ab, 0x8b2d50, 0x145247, 0x921ebc, 0x9e874a, 0x18cbb1, 0xe37b16, 0x6537ed, 0x69ae1b, 0xefe2e0, 0x709df7, 0xf6d10c, 0xfa48fa,
|
|
3093
|
+
0x7c0401, 0x42fa2f, 0xc4b6d4, 0xc82f22, 0x4e63d9, 0xd11cce, 0x575035, 0x5bc9c3, 0xdd8538,
|
|
3094
|
+
];
|
|
3095
|
+
|
|
3096
|
+
let crc24 = 0xb704ce; // init crc24 hash;
|
|
3097
|
+
valueToHash = Buffer.from(valueToHash); // convert value into buffer for processing
|
|
3098
|
+
for (let index = 0; index < valueToHash.length; index++) {
|
|
3099
|
+
crc24 = (crc24HashTable[((crc24 >> 16) ^ valueToHash[index]) & 0xff] ^ (crc24 << 8)) & 0xffffff;
|
|
3100
|
+
}
|
|
3101
|
+
return crc24.toString(16); // return crc24 as hex string
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
function scaleValue(value, sourceRangeMin, sourceRangeMax, targetRangeMin, targetRangeMax) {
|
|
3105
|
+
if (value < sourceRangeMin) {
|
|
3106
|
+
value = sourceRangeMin;
|
|
3107
|
+
}
|
|
3108
|
+
if (value > sourceRangeMax) {
|
|
3109
|
+
value = sourceRangeMax;
|
|
3110
|
+
}
|
|
3111
|
+
return ((value - sourceRangeMin) * (targetRangeMax - targetRangeMin)) / (sourceRangeMax - sourceRangeMin) + targetRangeMin;
|
|
3112
|
+
}
|