homebridge-nest-accfactory 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +14 -7
- package/config.schema.json +118 -0
- package/dist/HomeKitDevice.js +194 -77
- package/dist/HomeKitHistory.js +1 -1
- package/dist/config.js +207 -0
- package/dist/devices.js +113 -0
- package/dist/index.js +2 -1
- package/dist/nexustalk.js +19 -21
- package/dist/{camera.js → plugins/camera.js} +212 -239
- package/dist/{doorbell.js → plugins/doorbell.js} +32 -30
- package/dist/plugins/floodlight.js +91 -0
- package/dist/plugins/heatlink.js +17 -0
- package/dist/{protect.js → plugins/protect.js} +24 -41
- package/dist/{tempsensor.js → plugins/tempsensor.js} +13 -17
- package/dist/{thermostat.js → plugins/thermostat.js} +424 -381
- package/dist/{weather.js → plugins/weather.js} +26 -60
- package/dist/protobuf/nest/services/apigateway.proto +31 -1
- package/dist/protobuf/nest/trait/firmware.proto +207 -89
- package/dist/protobuf/nest/trait/hvac.proto +1052 -312
- package/dist/protobuf/nest/trait/located.proto +51 -8
- package/dist/protobuf/nest/trait/network.proto +366 -36
- package/dist/protobuf/nest/trait/occupancy.proto +145 -17
- package/dist/protobuf/nest/trait/product/protect.proto +57 -43
- package/dist/protobuf/nest/trait/resourcedirectory.proto +8 -0
- package/dist/protobuf/nest/trait/sensor.proto +7 -1
- package/dist/protobuf/nest/trait/service.proto +3 -1
- package/dist/protobuf/nest/trait/structure.proto +60 -14
- package/dist/protobuf/nest/trait/ui.proto +41 -1
- package/dist/protobuf/nest/trait/user.proto +6 -1
- package/dist/protobuf/nest/trait/voiceassistant.proto +2 -1
- package/dist/protobuf/nestlabs/eventingapi/v1.proto +20 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/protobuf/wdl.proto +18 -2
- package/dist/protobuf/weave/common.proto +2 -1
- package/dist/protobuf/weave/trait/heartbeat.proto +41 -1
- package/dist/protobuf/weave/trait/power.proto +1 -0
- package/dist/protobuf/weave/trait/security.proto +10 -1
- package/dist/streamer.js +68 -72
- package/dist/system.js +1208 -1245
- package/dist/webrtc.js +28 -23
- package/package.json +12 -12
- package/dist/floodlight.js +0 -97
package/dist/system.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Nest System communications
|
|
2
2
|
// Part of homebridge-nest-accfactory
|
|
3
3
|
//
|
|
4
|
-
// Code version 2025
|
|
4
|
+
// Code version 2025.06.13
|
|
5
5
|
// Mark Hulskamp
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
@@ -15,332 +15,144 @@ import { setInterval, clearInterval, setTimeout, clearTimeout } from 'node:timer
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import crypto from 'node:crypto';
|
|
18
|
-
import process from 'node:process';
|
|
19
|
-
import child_process from 'node:child_process';
|
|
20
18
|
import { fileURLToPath } from 'node:url';
|
|
21
19
|
|
|
22
20
|
// Import our modules
|
|
23
21
|
import HomeKitDevice from './HomeKitDevice.js';
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import NestFloodlight from './floodlight.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';
|
|
22
|
+
import { DeviceType, loadDeviceModules, getDeviceHKCategory } from './devices.js';
|
|
23
|
+
import { AccountType, processConfig, buildConnections } from './config.js';
|
|
31
24
|
|
|
25
|
+
// Define constants
|
|
32
26
|
const CAMERAALERTPOLLING = 2000; // Camera alerts polling timer
|
|
33
27
|
const CAMERAZONEPOLLING = 30000; // Camera zones changes polling timer
|
|
34
28
|
const WEATHERPOLLING = 300000; // Weather data polling timer
|
|
35
29
|
const NESTAPITIMEOUT = 10000; // Nest API timeout
|
|
36
30
|
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
31
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
32
|
+
const DATASOURCE = {
|
|
33
|
+
NestAPI: 'Nest', // From the Nest API
|
|
34
|
+
ProtobufAPI: 'Protobuf', // From the Protobuf API
|
|
35
|
+
};
|
|
40
36
|
|
|
41
37
|
// We handle the connections to Nest/Google
|
|
42
38
|
// Perform device management (additions/removals/updates)
|
|
43
39
|
export default class NestAccfactory {
|
|
44
|
-
static DeviceType = {
|
|
45
|
-
THERMOSTAT: 'thermostat',
|
|
46
|
-
TEMPSENSOR: 'temperature',
|
|
47
|
-
SMOKESENSOR: 'protect',
|
|
48
|
-
CAMERA: 'camera',
|
|
49
|
-
DOORBELL: 'doorbell',
|
|
50
|
-
FLOODLIGHT: 'floodlight',
|
|
51
|
-
WEATHER: 'weather',
|
|
52
|
-
LOCK: 'lock', // yet to implement
|
|
53
|
-
ALARM: 'alarm', // yet to implement
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
static DataSource = {
|
|
57
|
-
REST: 'REST', // From the REST API
|
|
58
|
-
PROTOBUF: 'Protobuf', // From the Protobuf API
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
static GoogleAccount = 'google'; // Google account connection
|
|
62
|
-
static NestAccount = 'nest'; // Nest account connection
|
|
63
|
-
|
|
64
40
|
cachedAccessories = []; // Track restored cached accessories
|
|
65
41
|
|
|
66
42
|
// Internal data only for this class
|
|
67
|
-
#connections =
|
|
68
|
-
#rawData = {}; // Cached copy of data from both
|
|
43
|
+
#connections = undefined; // Object of confirmed connections
|
|
44
|
+
#rawData = {}; // Cached copy of data from both Nest and Protobuf APIs
|
|
69
45
|
#eventEmitter = new EventEmitter(); // Used for object messaging from this platform
|
|
70
|
-
#connectionTimer = undefined;
|
|
71
46
|
#protobufRoot = null; // Protobuf loaded protos
|
|
72
47
|
#trackedDevices = {}; // Object of devices we've created. used to track data source type, comms uuid. key'd by serial #
|
|
48
|
+
#deviceModules = undefined; // No loaded device support modules to start
|
|
49
|
+
|
|
50
|
+
constructor(log, config, api, eventEmitter) {
|
|
51
|
+
// If no explicit event emitter was passed, and the api is an EventEmitter (e.g., in Homebridge),
|
|
52
|
+
// we'll treat it as the source for lifecycle messages like didFinishLaunching/shutdown in this constructor
|
|
53
|
+
if (api instanceof EventEmitter && eventEmitter === undefined) {
|
|
54
|
+
eventEmitter = api;
|
|
55
|
+
}
|
|
73
56
|
|
|
74
|
-
constructor(log, config, api) {
|
|
75
|
-
this.config = config;
|
|
76
57
|
this.log = log;
|
|
77
58
|
this.api = api;
|
|
78
59
|
|
|
79
60
|
// Perform validation on the configuration passed into us and set defaults if not present
|
|
61
|
+
this.config = processConfig(config, this.log);
|
|
62
|
+
this.#connections = buildConnections(this.config);
|
|
80
63
|
|
|
81
|
-
//
|
|
82
|
-
Object.keys(this.config).forEach((key) => {
|
|
83
|
-
if (this.config[key]?.access_token !== undefined && this.config[key].access_token !== '') {
|
|
84
|
-
// Nest account connection, assign a random UUID for each connection
|
|
85
|
-
this.#connections[crypto.randomUUID()] = {
|
|
86
|
-
type: NestAccfactory.NestAccount,
|
|
87
|
-
authorised: false,
|
|
88
|
-
access_token: this.config[key].access_token,
|
|
89
|
-
fieldTest: this.config[key]?.fieldTest === true,
|
|
90
|
-
referer: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
91
|
-
restAPIHost: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
92
|
-
cameraAPIHost: this.config[key]?.fieldTest === true ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
|
|
93
|
-
protobufAPIHost: this.config[key]?.fieldTest === true ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
if (
|
|
97
|
-
this.config[key]?.issuetoken !== undefined &&
|
|
98
|
-
this.config[key].issuetoken !== '' &&
|
|
99
|
-
this.config[key]?.cookie !== undefined &&
|
|
100
|
-
this.config[key].cookie !== ''
|
|
101
|
-
) {
|
|
102
|
-
// Google account connection, assign a random UUID for each connection
|
|
103
|
-
this.#connections[crypto.randomUUID()] = {
|
|
104
|
-
type: NestAccfactory.GoogleAccount,
|
|
105
|
-
authorised: false,
|
|
106
|
-
issuetoken: this.config[key].issuetoken,
|
|
107
|
-
cookie: this.config[key].cookie,
|
|
108
|
-
fieldTest: this.config[key]?.fieldTest === true,
|
|
109
|
-
referer: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
110
|
-
restAPIHost: this.config[key]?.fieldTest === true ? 'home.ft.nest.com' : 'home.nest.com',
|
|
111
|
-
cameraAPIHost: this.config[key]?.fieldTest === true ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
|
|
112
|
-
protobufAPIHost: this.config[key]?.fieldTest === true ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// If we don't have either a Nest access_token and/or a Google issuetoken/cookie, return back.
|
|
64
|
+
// Check for valid connections, either a Nest and/or Google one specified. Otherwise, return back.
|
|
118
65
|
if (Object.keys(this.#connections).length === 0) {
|
|
119
|
-
this?.log?.error
|
|
66
|
+
this?.log?.error?.('No connections have been specified in the JSON configuration. Please review');
|
|
120
67
|
return;
|
|
121
68
|
}
|
|
122
69
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
70
|
+
eventEmitter?.on?.('didFinishLaunching', async () => {
|
|
71
|
+
// We got notified that Homebridge (or Docker) has finished loading
|
|
126
72
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.config.options.weather = this.config.options?.weather === true;
|
|
130
|
-
this.config.options.hksv = this.config.options?.hksv === true;
|
|
131
|
-
|
|
132
|
-
// Controls what APIs we use, default is to use both REST and protobuf APIs
|
|
133
|
-
this.config.options.restAPI = this.config.options?.restAPI === true || this.config.options?.restAPI === undefined;
|
|
134
|
-
this.config.options.protobufAPI = this.config.options?.protobufAPI === true || this.config.options?.protobufAPI === undefined;
|
|
135
|
-
|
|
136
|
-
// Get configuration for max number of concurrent 'live view' streams. For HomeKit Secure Video, this will always be 1
|
|
137
|
-
this.config.options.maxStreams =
|
|
138
|
-
isNaN(this.config.options?.maxStreams) === false && this.deviceData?.hksv === false
|
|
139
|
-
? Number(this.config.options.maxStreams)
|
|
140
|
-
: this.deviceData?.hksv === true
|
|
141
|
-
? 1
|
|
142
|
-
: 2;
|
|
143
|
-
|
|
144
|
-
// Check if a ffmpeg binary exist via a specific path in configuration OR /usr/local/bin
|
|
145
|
-
this.config.options.ffmpeg = {};
|
|
146
|
-
this.config.options.ffmpeg.debug = this.config.options?.ffmpegDebug === true;
|
|
147
|
-
this.config.options.ffmpeg.binary = path.resolve(
|
|
148
|
-
typeof this.config.options?.ffmpegPath === 'string' && this.config.options.ffmpegPath !== ''
|
|
149
|
-
? this.config.options.ffmpegPath
|
|
150
|
-
: '/usr/local/bin',
|
|
151
|
-
);
|
|
73
|
+
// Load device support modules from the plugins folder if not already done
|
|
74
|
+
this.#deviceModules = await loadDeviceModules(this.log, 'plugins');
|
|
152
75
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
76
|
+
// Start reconnect loop per connection with backoff for failed tries
|
|
77
|
+
// This also initiates both Nest and Protobuf subscribes
|
|
78
|
+
for (const uuid of Object.keys(this.#connections)) {
|
|
79
|
+
let reconnectDelay = 15000;
|
|
157
80
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// If we flag ffmpegPath as undefined, no video streaming/record support enabled for camers/doorbells
|
|
171
|
-
this.config.options.ffmpeg.binary = undefined;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Process ffmpeg binary to see if we can use it
|
|
175
|
-
if (fs.existsSync(this.config.options.ffmpeg.binary) === true) {
|
|
176
|
-
let ffmpegProcess = child_process.spawnSync(this.config.options.ffmpeg.binary, ['-version'], {
|
|
177
|
-
env: process.env,
|
|
178
|
-
});
|
|
179
|
-
if (ffmpegProcess.stdout !== null) {
|
|
180
|
-
// Determine ffmpeg version
|
|
181
|
-
this.config.options.ffmpeg.version = ffmpegProcess.stdout
|
|
182
|
-
.toString()
|
|
183
|
-
.match(/(?:ffmpeg version:(\d+)\.)?(?:(\d+)\.)?(?:(\d+)\.\d+)(.*?)/gim)[0];
|
|
184
|
-
|
|
185
|
-
// Determine what libraries ffmpeg is compiled with
|
|
186
|
-
this.config.options.ffmpeg.libspeex = ffmpegProcess.stdout.toString().includes('--enable-libspeex') === true;
|
|
187
|
-
this.config.options.ffmpeg.libopus = ffmpegProcess.stdout.toString().includes('--enable-libopus') === true;
|
|
188
|
-
this.config.options.ffmpeg.libx264 = ffmpegProcess.stdout.toString().includes('--enable-libx264') === true;
|
|
189
|
-
this.config.options.ffmpeg.libfdk_aac = ffmpegProcess.stdout.toString().includes('--enable-libfdk-aac') === true;
|
|
190
|
-
if (
|
|
191
|
-
this.config.options.ffmpeg.version.localeCompare(FFMPEGVERSION, undefined, {
|
|
192
|
-
numeric: true,
|
|
193
|
-
sensitivity: 'case',
|
|
194
|
-
caseFirst: 'upper',
|
|
195
|
-
}) === -1 ||
|
|
196
|
-
this.config.options.ffmpeg.libspeex === false ||
|
|
197
|
-
this.config.options.ffmpeg.libopus === false ||
|
|
198
|
-
this.config.options.ffmpeg.libx264 === false ||
|
|
199
|
-
this.config.options.ffmpeg.libfdk_aac === false
|
|
200
|
-
) {
|
|
201
|
-
this?.log?.warn &&
|
|
202
|
-
this.log.warn('ffmpeg binary "%s" does not meet the minimum support requirements', this.config.options.ffmpeg.binary);
|
|
203
|
-
if (
|
|
204
|
-
this.config.options.ffmpeg.version.localeCompare(FFMPEGVERSION, undefined, {
|
|
205
|
-
numeric: true,
|
|
206
|
-
sensitivity: 'case',
|
|
207
|
-
caseFirst: 'upper',
|
|
208
|
-
}) === -1
|
|
209
|
-
) {
|
|
210
|
-
this?.log?.warn &&
|
|
211
|
-
this.log.warn(
|
|
212
|
-
'Minimum binary version is "%s", however the installed version is "%s"',
|
|
213
|
-
FFMPEGVERSION,
|
|
214
|
-
this.config.options.ffmpeg.version,
|
|
215
|
-
);
|
|
216
|
-
this?.log?.warn && this.log.warn('Stream video/recording from camera/doorbells will be unavailable');
|
|
81
|
+
const reconnectLoop = async () => {
|
|
82
|
+
if (this.#connections?.[uuid]?.authorised === false) {
|
|
83
|
+
try {
|
|
84
|
+
await this.#connect(uuid);
|
|
85
|
+
this.#subscribeNest(uuid, true);
|
|
86
|
+
this.#subscribeProtobuf(uuid, true);
|
|
87
|
+
// eslint-disable-next-line no-unused-vars
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// Empty
|
|
90
|
+
}
|
|
217
91
|
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.config.options.ffmpeg.libspeex === false &&
|
|
222
|
-
this.config.options.ffmpeg.libx264 === true &&
|
|
223
|
-
this.config.options.ffmpeg.libfdk_aac === true
|
|
224
|
-
) {
|
|
225
|
-
this?.log?.warn && this.log.warn('Missing libspeex in ffmpeg binary, talkback on certain camera/doorbells will be unavailable');
|
|
226
|
-
}
|
|
227
|
-
if (
|
|
228
|
-
this.config.options.ffmpeg.libx264 === true &&
|
|
229
|
-
this.config.options.ffmpeg.libfdk_aac === false &&
|
|
230
|
-
this.config.options.ffmpeg.libopus === false
|
|
231
|
-
) {
|
|
232
|
-
this?.log?.warn &&
|
|
233
|
-
this.log.warn('Missing libfdk_aac and libopus in ffmpeg binary, audio from camera/doorbells will be unavailable');
|
|
234
|
-
}
|
|
235
|
-
if (this.config.options.ffmpeg.libx264 === true && this.config.options.ffmpeg.libfdk_aac === false) {
|
|
236
|
-
this?.log?.warn && this.log.warn('Missing libfdk_aac in ffmpeg binary, audio from camera/doorbells will be unavailable');
|
|
237
|
-
}
|
|
238
|
-
if (
|
|
239
|
-
this.config.options.ffmpeg.libx264 === true &&
|
|
240
|
-
this.config.options.ffmpeg.libfdk_aac === true &&
|
|
241
|
-
this.config.options.ffmpeg.libopus === false
|
|
242
|
-
) {
|
|
243
|
-
this?.log?.warn &&
|
|
244
|
-
this.log.warn(
|
|
245
|
-
'Missing libopus in ffmpeg binary, audio (including talkback) from certain camera/doorbells will be unavailable',
|
|
246
|
-
);
|
|
92
|
+
reconnectDelay = this.#connections?.[uuid]?.authorised === true ? 15000 : Math.min(reconnectDelay * 2, 60000);
|
|
93
|
+
} else {
|
|
94
|
+
reconnectDelay = 15000;
|
|
247
95
|
}
|
|
248
|
-
if (this.config.options.ffmpeg.libx264 === false) {
|
|
249
|
-
this?.log?.warn &&
|
|
250
|
-
this.log.warn('Missing libx264 in ffmpeg binary, stream video/recording from camera/doorbells will be unavailable');
|
|
251
96
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
97
|
+
setTimeout(reconnectLoop, reconnectDelay);
|
|
98
|
+
};
|
|
257
99
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
100
|
+
reconnectLoop();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
261
103
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
104
|
+
eventEmitter?.on?.('shutdown', async () => {
|
|
105
|
+
// We got notified that Homebridge is shutting down
|
|
106
|
+
// Perform cleanup of internal state
|
|
107
|
+
this.#eventEmitter?.removeAllListeners();
|
|
266
108
|
|
|
267
|
-
|
|
268
|
-
clearInterval(
|
|
269
|
-
this.#connectionTimer = setInterval(this.discoverDevices.bind(this), 15000);
|
|
109
|
+
Object.values(this.#trackedDevices).forEach((device) => {
|
|
110
|
+
Object.values(device?.timers || {}).forEach((timer) => clearInterval(timer));
|
|
270
111
|
});
|
|
271
112
|
|
|
272
|
-
this
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (value?.timers !== undefined) {
|
|
278
|
-
Object.values(value?.timers).forEach((timers) => {
|
|
279
|
-
clearInterval(timers);
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
this.#trackedDevices = {};
|
|
284
|
-
clearInterval(this.#connectionTimer);
|
|
285
|
-
this.#connectionTimer = undefined;
|
|
286
|
-
this.#rawData = {};
|
|
287
|
-
this.#protobufRoot = null;
|
|
288
|
-
this.#eventEmitter = undefined;
|
|
289
|
-
});
|
|
290
|
-
}
|
|
113
|
+
this.#trackedDevices = {};
|
|
114
|
+
this.#rawData = {};
|
|
115
|
+
this.#protobufRoot = null;
|
|
116
|
+
this.#eventEmitter = undefined;
|
|
117
|
+
});
|
|
291
118
|
|
|
292
119
|
// Setup event listeners for set/get calls from devices if not already done so
|
|
293
120
|
this.#eventEmitter.addListener(HomeKitDevice.SET, (uuid, values) => {
|
|
294
|
-
this.#set(
|
|
121
|
+
this.#set(values);
|
|
295
122
|
});
|
|
123
|
+
|
|
296
124
|
this.#eventEmitter.addListener(HomeKitDevice.GET, async (uuid, values) => {
|
|
297
|
-
let results = await this.#get(
|
|
125
|
+
let results = await this.#get(values);
|
|
298
126
|
// Send the results back to the device via a special event (only if still active)
|
|
299
|
-
|
|
300
|
-
this.#eventEmitter.emit(HomeKitDevice.GET + '->' + uuid, results);
|
|
301
|
-
}
|
|
127
|
+
this.#eventEmitter?.emit?.(HomeKitDevice.GET + '->' + uuid, results);
|
|
302
128
|
});
|
|
303
129
|
}
|
|
304
130
|
|
|
305
131
|
configureAccessory(accessory) {
|
|
306
132
|
// This gets called from Homebridge each time it restores an accessory from its cache
|
|
307
|
-
this?.log?.info
|
|
133
|
+
this?.log?.info?.('Loading accessory from cache:', accessory.displayName);
|
|
308
134
|
|
|
309
135
|
// add the restored accessory to the accessories cache, so we can track if it has already been registered
|
|
310
136
|
this.cachedAccessories.push(accessory);
|
|
311
137
|
}
|
|
312
138
|
|
|
313
|
-
async
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.#subscribeProtobuf(uuid, true);
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
async #connect(connectionUUID) {
|
|
329
|
-
if (typeof this.#connections?.[connectionUUID] === 'object') {
|
|
330
|
-
if (this.#connections[connectionUUID].type === NestAccfactory.GoogleAccount) {
|
|
139
|
+
async #connect(uuid) {
|
|
140
|
+
if (typeof this.#connections?.[uuid] === 'object') {
|
|
141
|
+
this?.log?.info?.(
|
|
142
|
+
'Performing authorisation for connection "%s" %s',
|
|
143
|
+
this.#connections[uuid].name,
|
|
144
|
+
this.#connections[uuid].fieldTest === true ? 'using field test endpoints' : '',
|
|
145
|
+
);
|
|
146
|
+
if (this.#connections[uuid].type === AccountType.Google) {
|
|
331
147
|
// Google cookie method as refresh token method no longer supported by Google since October 2022
|
|
332
148
|
// Instructions from homebridge_nest or homebridge_nest_cam to obtain this
|
|
333
|
-
this?.log?.
|
|
334
|
-
this.log.info(
|
|
335
|
-
'Performing Google account authorisation ' +
|
|
336
|
-
(this.#connections[connectionUUID].fieldTest === true ? 'using field test endpoints' : ''),
|
|
337
|
-
);
|
|
149
|
+
this?.log?.debug?.('Performing authorisation using Google account for connection uuid "%s"', uuid);
|
|
338
150
|
|
|
339
|
-
await fetchWrapper('get', this.#connections[
|
|
151
|
+
await fetchWrapper('get', this.#connections[uuid].issuetoken, {
|
|
340
152
|
headers: {
|
|
341
153
|
referer: 'https://accounts.google.com/o/oauth2/iframe',
|
|
342
154
|
'User-Agent': USERAGENT,
|
|
343
|
-
cookie: this.#connections[
|
|
155
|
+
cookie: this.#connections[uuid].cookie,
|
|
344
156
|
'Sec-Fetch-Mode': 'cors',
|
|
345
157
|
'X-Requested-With': 'XmlHttpRequest',
|
|
346
158
|
},
|
|
@@ -354,7 +166,7 @@ export default class NestAccfactory {
|
|
|
354
166
|
'https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt',
|
|
355
167
|
{
|
|
356
168
|
headers: {
|
|
357
|
-
referer: 'https://' + this.#connections[
|
|
169
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
358
170
|
'User-Agent': USERAGENT,
|
|
359
171
|
Authorization: data.token_type + ' ' + data.access_token,
|
|
360
172
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -369,9 +181,9 @@ export default class NestAccfactory {
|
|
|
369
181
|
let googleToken = data.jwt;
|
|
370
182
|
let tokenExpire = Math.floor(new Date(data.claims.expirationTime).valueOf() / 1000); // Token expiry, should be 1hr
|
|
371
183
|
|
|
372
|
-
await fetchWrapper('get', 'https://' + this.#connections[
|
|
184
|
+
await fetchWrapper('get', 'https://' + this.#connections[uuid].restAPIHost + '/session', {
|
|
373
185
|
headers: {
|
|
374
|
-
referer: 'https://' + this.#connections[
|
|
186
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
375
187
|
'User-Agent': USERAGENT,
|
|
376
188
|
Authorization: 'Basic ' + googleToken,
|
|
377
189
|
},
|
|
@@ -379,65 +191,58 @@ export default class NestAccfactory {
|
|
|
379
191
|
.then((response) => response.json())
|
|
380
192
|
.then((data) => {
|
|
381
193
|
// Store successful connection details
|
|
382
|
-
this.#connections[
|
|
383
|
-
this.#connections[
|
|
384
|
-
this.#connections[
|
|
385
|
-
this.#connections[
|
|
386
|
-
this.#connections[
|
|
387
|
-
this.#connections[
|
|
194
|
+
this.#connections[uuid].authorised = true;
|
|
195
|
+
this.#connections[uuid].userID = data.userid;
|
|
196
|
+
this.#connections[uuid].transport_url = data.urls.transport_url;
|
|
197
|
+
this.#connections[uuid].weather_url = data.urls.weather_url;
|
|
198
|
+
this.#connections[uuid].token = googleToken;
|
|
199
|
+
this.#connections[uuid].cameraAPI = {
|
|
388
200
|
key: 'Authorization',
|
|
389
201
|
value: 'Basic ', // NOTE: extra space required
|
|
390
202
|
token: googleToken,
|
|
391
203
|
oauth2: googleOAuth2Token,
|
|
204
|
+
fieldTest: this.#connections[uuid]?.fieldTest === true,
|
|
392
205
|
};
|
|
393
206
|
|
|
394
207
|
// Set timeout for token expiry refresh
|
|
395
|
-
clearTimeout(this.#connections[
|
|
396
|
-
this.#connections[
|
|
208
|
+
clearTimeout(this.#connections[uuid].timer);
|
|
209
|
+
this.#connections[uuid].timer = setTimeout(
|
|
397
210
|
() => {
|
|
398
|
-
this?.log?.info
|
|
399
|
-
this.#connect(
|
|
211
|
+
this?.log?.info?.('Performing periodic token refresh for connection "%s"', this.#connections[uuid].name);
|
|
212
|
+
this.#connect(uuid);
|
|
400
213
|
},
|
|
401
214
|
(tokenExpire - Math.floor(Date.now() / 1000) - 60) * 1000,
|
|
402
215
|
); // Refresh just before token expiry
|
|
403
216
|
|
|
404
|
-
this?.log?.success
|
|
217
|
+
this?.log?.success?.('Successfully authorised connection "%s"', this.#connections[uuid].name);
|
|
405
218
|
});
|
|
406
219
|
});
|
|
407
220
|
})
|
|
408
221
|
// eslint-disable-next-line no-unused-vars
|
|
409
222
|
.catch((error) => {
|
|
410
223
|
// The token we used to obtained a Nest session failed, so overall authorisation failed
|
|
411
|
-
this.#connections[
|
|
412
|
-
this?.log?.debug
|
|
413
|
-
|
|
414
|
-
'Failed to connect to gateway using credential details for connection uuid "%s". A periodic retry event will be triggered',
|
|
415
|
-
connectionUUID,
|
|
416
|
-
);
|
|
417
|
-
this?.log?.error && this.log.error('Authorisation failed using Google account');
|
|
224
|
+
this.#connections[uuid].authorised = false;
|
|
225
|
+
this?.log?.debug?.('Failed to connect using credential details for connection uuid "%s"', uuid);
|
|
226
|
+
this?.log?.error?.('Authorisation failed on connection "%s"', this.#connections[uuid].name);
|
|
418
227
|
});
|
|
419
228
|
}
|
|
420
229
|
|
|
421
|
-
if (this.#connections[
|
|
230
|
+
if (this.#connections[uuid].type === AccountType.Nest) {
|
|
422
231
|
// Nest access token method. Get WEBSITE2 cookie for use with camera API calls if needed later
|
|
423
|
-
this?.log?.
|
|
424
|
-
this.log.info(
|
|
425
|
-
'Performing Nest account authorisation ' +
|
|
426
|
-
(this.#connections[connectionUUID].fieldTest === true ? 'using field test endpoints' : ''),
|
|
427
|
-
);
|
|
232
|
+
this?.log?.debug?.('Performing authorisation using Nest account for connection uuid "%s"', uuid);
|
|
428
233
|
|
|
429
234
|
await fetchWrapper(
|
|
430
235
|
'post',
|
|
431
|
-
'https://webapi.' + this.#connections[
|
|
236
|
+
'https://webapi.' + this.#connections[uuid].cameraAPIHost + '/api/v1/login.login_nest',
|
|
432
237
|
{
|
|
433
238
|
withCredentials: true,
|
|
434
239
|
headers: {
|
|
435
|
-
referer: 'https://' + this.#connections[
|
|
240
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
436
241
|
'User-Agent': USERAGENT,
|
|
437
242
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
438
243
|
},
|
|
439
244
|
},
|
|
440
|
-
Buffer.from('access_token=' + this.#connections[
|
|
245
|
+
Buffer.from('access_token=' + this.#connections[uuid].access_token, 'utf8'),
|
|
441
246
|
)
|
|
442
247
|
.then((response) => response.json())
|
|
443
248
|
.then(async (data) => {
|
|
@@ -447,53 +252,58 @@ export default class NestAccfactory {
|
|
|
447
252
|
|
|
448
253
|
let nestToken = data.items[0].session_token;
|
|
449
254
|
|
|
450
|
-
await fetchWrapper('get', 'https://' + this.#connections[
|
|
255
|
+
await fetchWrapper('get', 'https://' + this.#connections[uuid].restAPIHost + '/session', {
|
|
451
256
|
headers: {
|
|
452
|
-
referer: 'https://' + this.#connections[
|
|
257
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
453
258
|
'User-Agent': USERAGENT,
|
|
454
|
-
Authorization: 'Basic ' + this.#connections[
|
|
259
|
+
Authorization: 'Basic ' + this.#connections[uuid].access_token,
|
|
455
260
|
},
|
|
456
261
|
})
|
|
457
262
|
.then((response) => response.json())
|
|
458
263
|
.then((data) => {
|
|
459
264
|
// Store successful connection details
|
|
460
|
-
this.#connections[
|
|
461
|
-
this.#connections[
|
|
462
|
-
this.#connections[
|
|
463
|
-
this.#connections[
|
|
464
|
-
this.#connections[
|
|
465
|
-
this.#connections[
|
|
265
|
+
this.#connections[uuid].authorised = true;
|
|
266
|
+
this.#connections[uuid].userID = data.userid;
|
|
267
|
+
this.#connections[uuid].transport_url = data.urls.transport_url;
|
|
268
|
+
this.#connections[uuid].weather_url = data.urls.weather_url;
|
|
269
|
+
this.#connections[uuid].token = this.#connections[uuid].access_token;
|
|
270
|
+
this.#connections[uuid].cameraAPI = {
|
|
466
271
|
key: 'cookie',
|
|
467
|
-
value: this.#connections[
|
|
272
|
+
value: this.#connections[uuid].fieldTest === true ? 'website_ft=' : 'website_2=',
|
|
468
273
|
token: nestToken,
|
|
274
|
+
fieldTest: this.#connections[uuid]?.fieldTest === true,
|
|
469
275
|
};
|
|
470
276
|
|
|
471
277
|
// Set timeout for token expiry refresh
|
|
472
|
-
clearTimeout(this.#connections[
|
|
473
|
-
this.#connections[
|
|
278
|
+
clearTimeout(this.#connections[uuid].timer);
|
|
279
|
+
this.#connections[uuid].timer = setTimeout(
|
|
474
280
|
() => {
|
|
475
|
-
this?.log?.info
|
|
476
|
-
this.#connect(
|
|
281
|
+
this?.log?.info?.('Performing periodic token refresh for connection "%s"', this.#connections[uuid].name);
|
|
282
|
+
this.#connect(uuid);
|
|
477
283
|
},
|
|
478
284
|
1000 * 3600 * 24,
|
|
479
285
|
); // Refresh token every 24hrs
|
|
480
286
|
|
|
481
|
-
this?.log?.success
|
|
287
|
+
this?.log?.success?.('Successfully authorised connection "%s"', this.#connections[uuid].name);
|
|
482
288
|
});
|
|
483
289
|
})
|
|
484
290
|
// eslint-disable-next-line no-unused-vars
|
|
485
291
|
.catch((error) => {
|
|
486
292
|
// The token we used to obtained a Nest session failed, so overall authorisation failed
|
|
487
|
-
this.#connections[
|
|
488
|
-
this?.log?.debug
|
|
489
|
-
this?.log?.error
|
|
293
|
+
this.#connections[uuid].authorised = false;
|
|
294
|
+
this?.log?.debug?.('Failed to connect using credential details for connection uuid "%s"', uuid);
|
|
295
|
+
this?.log?.error?.('Authorisation failed on connection "%s"', this.#connections[uuid].name);
|
|
490
296
|
});
|
|
491
297
|
}
|
|
492
298
|
}
|
|
493
299
|
}
|
|
494
300
|
|
|
495
|
-
async #
|
|
496
|
-
if (
|
|
301
|
+
async #subscribeNest(uuid, fullRefresh) {
|
|
302
|
+
if (
|
|
303
|
+
typeof this.#connections?.[uuid] !== 'object' ||
|
|
304
|
+
this.#connections?.[uuid]?.authorised === false ||
|
|
305
|
+
this.config?.options?.useNestAPI === false
|
|
306
|
+
) {
|
|
497
307
|
// Not a valid connection object and/or we're not authorised
|
|
498
308
|
return;
|
|
499
309
|
}
|
|
@@ -513,25 +323,20 @@ export default class NestAccfactory {
|
|
|
513
323
|
'topaz',
|
|
514
324
|
'widget_track',
|
|
515
325
|
'quartz',
|
|
326
|
+
'occupancy',
|
|
516
327
|
];
|
|
517
328
|
|
|
518
|
-
// By default, setup for a full data read from the
|
|
519
|
-
let subscribeURL =
|
|
520
|
-
'https://' +
|
|
521
|
-
this.#connections[connectionUUID].restAPIHost +
|
|
522
|
-
'/api/0.1/user/' +
|
|
523
|
-
this.#connections[connectionUUID].userID +
|
|
524
|
-
'/app_launch';
|
|
329
|
+
// By default, setup for a full data read from the Nest API
|
|
330
|
+
let subscribeURL = 'https://' + this.#connections[uuid].restAPIHost + '/api/0.1/user/' + this.#connections[uuid].userID + '/app_launch';
|
|
525
331
|
let subscribeJSONData = { known_bucket_types: REQUIREDBUCKETS, known_bucket_versions: [] };
|
|
526
332
|
|
|
527
333
|
if (fullRefresh === false) {
|
|
528
|
-
// We have data stored from this
|
|
529
|
-
subscribeURL = this.#connections[
|
|
334
|
+
// We have data stored from this Nest API, so setup read using known object
|
|
335
|
+
subscribeURL = this.#connections[uuid].transport_url + '/v6/subscribe';
|
|
530
336
|
subscribeJSONData = { objects: [] };
|
|
531
|
-
|
|
532
337
|
Object.entries(this.#rawData)
|
|
533
338
|
// eslint-disable-next-line no-unused-vars
|
|
534
|
-
.filter(([object_key, object]) => object.source ===
|
|
339
|
+
.filter(([object_key, object]) => object.source === DATASOURCE.NestAPI && object.connection === uuid)
|
|
535
340
|
.forEach(([object_key, object]) => {
|
|
536
341
|
subscribeJSONData.objects.push({
|
|
537
342
|
object_key: object_key,
|
|
@@ -542,7 +347,7 @@ export default class NestAccfactory {
|
|
|
542
347
|
}
|
|
543
348
|
|
|
544
349
|
if (fullRefresh === true) {
|
|
545
|
-
this?.log?.debug
|
|
350
|
+
this?.log?.debug?.('Starting Nest API subscribe for connection uuid "%s"', uuid);
|
|
546
351
|
}
|
|
547
352
|
|
|
548
353
|
fetchWrapper(
|
|
@@ -550,9 +355,9 @@ export default class NestAccfactory {
|
|
|
550
355
|
subscribeURL,
|
|
551
356
|
{
|
|
552
357
|
headers: {
|
|
553
|
-
referer: 'https://' + this.#connections[
|
|
358
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
554
359
|
'User-Agent': USERAGENT,
|
|
555
|
-
Authorization: 'Basic ' + this.#connections[
|
|
360
|
+
Authorization: 'Basic ' + this.#connections[uuid].token,
|
|
556
361
|
},
|
|
557
362
|
keepalive: true,
|
|
558
363
|
//timeout: (5 * 60000),
|
|
@@ -585,12 +390,7 @@ export default class NestAccfactory {
|
|
|
585
390
|
) {
|
|
586
391
|
value.value.weather = this.#rawData[value.object_key].value.weather;
|
|
587
392
|
}
|
|
588
|
-
value.value.weather = await this.#
|
|
589
|
-
connectionUUID,
|
|
590
|
-
value.object_key,
|
|
591
|
-
value.value.latitude,
|
|
592
|
-
value.value.longitude,
|
|
593
|
-
);
|
|
393
|
+
value.value.weather = await this.#getWeather(uuid, value.object_key, value.value.latitude, value.value.longitude);
|
|
594
394
|
|
|
595
395
|
// Check for changes in the swarm property. This seems indicate changes in devices
|
|
596
396
|
if (typeof this.#rawData[value.object_key] === 'object') {
|
|
@@ -611,78 +411,70 @@ export default class NestAccfactory {
|
|
|
611
411
|
? this.#rawData[value.object_key].value.properties
|
|
612
412
|
: [];
|
|
613
413
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
this.#connections[
|
|
414
|
+
try {
|
|
415
|
+
let response = await fetchWrapper(
|
|
416
|
+
'get',
|
|
417
|
+
'https://webapi.' +
|
|
418
|
+
this.#connections[uuid].cameraAPIHost +
|
|
419
|
+
'/api/cameras.get_with_properties?uuid=' +
|
|
420
|
+
value.object_key.split('.')[1],
|
|
421
|
+
{
|
|
422
|
+
headers: {
|
|
423
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
424
|
+
'User-Agent': USERAGENT,
|
|
425
|
+
[this.#connections[uuid].cameraAPI.key]:
|
|
426
|
+
this.#connections[uuid].cameraAPI.value + this.#connections[uuid].cameraAPI.token,
|
|
427
|
+
},
|
|
428
|
+
timeout: NESTAPITIMEOUT,
|
|
626
429
|
},
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
.
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
this?.log?.debug
|
|
639
|
-
) {
|
|
640
|
-
this.log.debug('REST API had error retrieving camera/doorbell details during subscribe. Error was "%s"', error?.code);
|
|
641
|
-
}
|
|
642
|
-
});
|
|
430
|
+
);
|
|
431
|
+
let data = await response.json();
|
|
432
|
+
value.value.properties = data.items[0].properties;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error?.cause !== undefined && String(error.cause).toUpperCase().includes('TIMEOUT') === false) {
|
|
435
|
+
this?.log?.debug?.(
|
|
436
|
+
'Nest API had error retrieving camera/doorbell details during subscribe. Error was "%s"',
|
|
437
|
+
error?.code ?? String(error),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
643
441
|
|
|
644
442
|
value.value.activity_zones =
|
|
645
443
|
typeof this.#rawData[value.object_key]?.value?.activity_zones === 'object'
|
|
646
444
|
? this.#rawData[value.object_key].value.activity_zones
|
|
647
445
|
: [];
|
|
648
446
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
'
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
error?.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.log.debug(
|
|
681
|
-
'REST API had error retrieving camera/doorbell activity zones during subscribe. Error was "%s"',
|
|
682
|
-
error?.code,
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
});
|
|
447
|
+
try {
|
|
448
|
+
let response = await fetchWrapper(
|
|
449
|
+
'get',
|
|
450
|
+
value.value.nexus_api_http_server_url + '/cuepoint_category/' + value.object_key.split('.')[1],
|
|
451
|
+
{
|
|
452
|
+
headers: {
|
|
453
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
454
|
+
'User-Agent': USERAGENT,
|
|
455
|
+
[this.#connections[uuid].cameraAPI.key]:
|
|
456
|
+
this.#connections[uuid].cameraAPI.value + this.#connections[uuid].cameraAPI.token,
|
|
457
|
+
},
|
|
458
|
+
timeout: NESTAPITIMEOUT,
|
|
459
|
+
},
|
|
460
|
+
);
|
|
461
|
+
let data = await response.json();
|
|
462
|
+
value.value.activity_zones = data
|
|
463
|
+
.filter((zone) => zone?.type?.toUpperCase() === 'ACTIVITY' || zone?.type?.toUpperCase() === 'REGION')
|
|
464
|
+
.map((zone) => ({
|
|
465
|
+
id: zone.id === 0 ? 1 : zone.id,
|
|
466
|
+
name: makeHomeKitName(zone.label),
|
|
467
|
+
hidden: zone.hidden === true,
|
|
468
|
+
uri: zone.nexusapi_image_uri,
|
|
469
|
+
}));
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (error?.cause !== undefined && String(error.cause).toUpperCase().includes('TIMEOUT') === false) {
|
|
472
|
+
this?.log?.debug?.(
|
|
473
|
+
'Nest API had error retrieving camera/doorbell activity zones during subscribe. Error was "%s"',
|
|
474
|
+
error?.code ?? String(error),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
686
478
|
}
|
|
687
479
|
|
|
688
480
|
if (value.object_key.startsWith('buckets.') === true) {
|
|
@@ -693,7 +485,7 @@ export default class NestAccfactory {
|
|
|
693
485
|
// Check for added objects
|
|
694
486
|
value.value.buckets.map((object_key) => {
|
|
695
487
|
if (this.#rawData[value.object_key].value.buckets.includes(object_key) === false) {
|
|
696
|
-
// Since this is an added object to the raw
|
|
488
|
+
// Since this is an added object to the raw Nest API structure, we need to do a full read of the data
|
|
697
489
|
fullRefresh = true;
|
|
698
490
|
}
|
|
699
491
|
});
|
|
@@ -719,13 +511,11 @@ export default class NestAccfactory {
|
|
|
719
511
|
});
|
|
720
512
|
|
|
721
513
|
// Send removed notice onto HomeKit device for it to process
|
|
722
|
-
|
|
723
|
-
this.#
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
);
|
|
728
|
-
}
|
|
514
|
+
this.#eventEmitter?.emit?.(
|
|
515
|
+
this.#trackedDevices[this.#rawData[object_key].value.serial_number].uuid,
|
|
516
|
+
HomeKitDevice.REMOVE,
|
|
517
|
+
{},
|
|
518
|
+
);
|
|
729
519
|
|
|
730
520
|
// Finally, remove from tracked devices
|
|
731
521
|
delete this.#trackedDevices[this.#rawData[object_key].value.serial_number];
|
|
@@ -737,17 +527,17 @@ export default class NestAccfactory {
|
|
|
737
527
|
}
|
|
738
528
|
}
|
|
739
529
|
|
|
740
|
-
// Store or update the date in our internally saved raw
|
|
530
|
+
// Store or update the date in our internally saved raw Nest API data
|
|
741
531
|
if (typeof this.#rawData[value.object_key] === 'undefined') {
|
|
742
532
|
this.#rawData[value.object_key] = {};
|
|
743
533
|
this.#rawData[value.object_key].object_revision = value.object_revision;
|
|
744
534
|
this.#rawData[value.object_key].object_timestamp = value.object_timestamp;
|
|
745
|
-
this.#rawData[value.object_key].connection =
|
|
746
|
-
this.#rawData[value.object_key].source =
|
|
535
|
+
this.#rawData[value.object_key].connection = uuid;
|
|
536
|
+
this.#rawData[value.object_key].source = DATASOURCE.NestAPI;
|
|
747
537
|
this.#rawData[value.object_key].value = {};
|
|
748
538
|
}
|
|
749
539
|
|
|
750
|
-
// Finally, update our internal raw
|
|
540
|
+
// Finally, update our internal raw Nest API data with the new values
|
|
751
541
|
this.#rawData[value.object_key].object_revision = value.object_revision; // Used for future subscribes
|
|
752
542
|
this.#rawData[value.object_key].object_timestamp = value.object_timestamp; // Used for future subscribes
|
|
753
543
|
for (const [fieldKey, fieldValue] of Object.entries(value.value)) {
|
|
@@ -759,55 +549,57 @@ export default class NestAccfactory {
|
|
|
759
549
|
await this.#processPostSubscribe();
|
|
760
550
|
})
|
|
761
551
|
.catch((error) => {
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
552
|
+
if (
|
|
553
|
+
error?.cause === undefined ||
|
|
554
|
+
(typeof error.cause === 'object' && String(error.cause).toUpperCase().includes('TIMEOUT') === false)
|
|
555
|
+
) {
|
|
556
|
+
this?.log?.debug?.(
|
|
557
|
+
'Nest API had an error performing subscription with connection uuid "%s"',
|
|
558
|
+
uuid,
|
|
559
|
+
error?.message ?? String(error),
|
|
560
|
+
);
|
|
561
|
+
this?.log?.debug?.('Restarting Nest API subscription for connection uuid "%s"', uuid);
|
|
765
562
|
}
|
|
766
563
|
})
|
|
767
564
|
.finally(() => {
|
|
768
|
-
|
|
769
|
-
setTimeout(this.#subscribeREST.bind(this, connectionUUID, fullRefresh), 1000);
|
|
565
|
+
setTimeout(() => this.#subscribeNest(uuid, fullRefresh), 1000);
|
|
770
566
|
});
|
|
771
567
|
}
|
|
772
568
|
|
|
773
|
-
async #subscribeProtobuf(
|
|
774
|
-
if (
|
|
569
|
+
async #subscribeProtobuf(uuid, firstRun) {
|
|
570
|
+
if (
|
|
571
|
+
typeof this.#connections?.[uuid] !== 'object' ||
|
|
572
|
+
this.#connections?.[uuid]?.authorised === false ||
|
|
573
|
+
this.config?.options?.useGoogleAPI === false
|
|
574
|
+
) {
|
|
775
575
|
// Not a valid connection object and/or we're not authorised
|
|
776
576
|
return;
|
|
777
577
|
}
|
|
778
578
|
|
|
779
579
|
const calculate_message_size = (inputBuffer) => {
|
|
780
|
-
// First byte in the is a tag type??
|
|
781
|
-
// Following is a varint type
|
|
782
|
-
// After varint size, is the buffer content
|
|
783
580
|
let varint = 0;
|
|
784
|
-
let
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
varint |= (
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
throw new Error('VarInt exceeds allowed bounds.');
|
|
793
|
-
}
|
|
794
|
-
if ((currentByte & 0x80) !== 0x80) {
|
|
795
|
-
break;
|
|
581
|
+
let shift = 0;
|
|
582
|
+
|
|
583
|
+
for (let i = 1; i <= 5; i++) {
|
|
584
|
+
// Start at index 1 (skip tag byte)
|
|
585
|
+
let byte = inputBuffer[i];
|
|
586
|
+
varint |= (byte & 0x7f) << shift;
|
|
587
|
+
if ((byte & 0x80) === 0) {
|
|
588
|
+
return varint + i + 1; // +1 to include initial tag byte
|
|
796
589
|
}
|
|
590
|
+
shift += 7;
|
|
797
591
|
}
|
|
798
592
|
|
|
799
|
-
|
|
800
|
-
return varint + bufferPos + 1;
|
|
593
|
+
throw new Error('VarInt exceeds allowed bounds.');
|
|
801
594
|
};
|
|
802
595
|
|
|
803
|
-
const traverseTypes = (
|
|
804
|
-
if (
|
|
805
|
-
callback(
|
|
596
|
+
const traverseTypes = (trait, callback) => {
|
|
597
|
+
if (trait instanceof protobuf.Type === true) {
|
|
598
|
+
callback(trait);
|
|
806
599
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
});
|
|
600
|
+
|
|
601
|
+
for (const nested of trait && trait.nestedArray ? trait.nestedArray : []) {
|
|
602
|
+
traverseTypes(nested, callback);
|
|
811
603
|
}
|
|
812
604
|
};
|
|
813
605
|
|
|
@@ -817,13 +609,12 @@ export default class NestAccfactory {
|
|
|
817
609
|
protobuf.configure();
|
|
818
610
|
this.#protobufRoot = protobuf.loadSync(path.resolve(__dirname + '/protobuf/root.proto'));
|
|
819
611
|
if (this.#protobufRoot !== null) {
|
|
820
|
-
this?.log?.debug
|
|
612
|
+
this?.log?.debug?.('Loaded protobuf support files for Protobuf API');
|
|
821
613
|
}
|
|
822
614
|
}
|
|
823
615
|
|
|
824
616
|
if (this.#protobufRoot === null) {
|
|
825
|
-
this?.log?.warn
|
|
826
|
-
this.log.warn('Failed to loaded Protobuf API support files. This will cause certain Nest/Google devices to be un-supported');
|
|
617
|
+
this?.log?.warn?.('Failed to loaded Protobuf API support files. This will cause certain Nest/Google devices to be un-supported');
|
|
827
618
|
return;
|
|
828
619
|
}
|
|
829
620
|
|
|
@@ -834,15 +625,15 @@ export default class NestAccfactory {
|
|
|
834
625
|
let observeRequest = this.#protobufRoot.lookup('nestlabs.gateway.v2.ObserveRequest');
|
|
835
626
|
if (traitTypeObserveParam !== null && observeRequest !== null) {
|
|
836
627
|
traverseTypes(this.#protobufRoot, (type) => {
|
|
837
|
-
// We only want to have certain trait'families' in our observe reponse we are building
|
|
628
|
+
// We only want to have certain trait 'families' in our observe reponse we are building
|
|
838
629
|
// This also depends on the account type we connected with
|
|
839
630
|
// Nest accounts cannot observe camera/doorbell product traits
|
|
840
631
|
if (
|
|
841
|
-
(this.#connections[
|
|
632
|
+
(this.#connections[uuid].type === AccountType.Nest &&
|
|
842
633
|
type.fullName.startsWith('.nest.trait.product.camera') === false &&
|
|
843
634
|
type.fullName.startsWith('.nest.trait.product.doorbell') === false &&
|
|
844
635
|
(type.fullName.startsWith('.nest.trait') === true || type.fullName.startsWith('.weave.') === true)) ||
|
|
845
|
-
(this.#connections[
|
|
636
|
+
(this.#connections[uuid].type === AccountType.Google &&
|
|
846
637
|
(type.fullName.startsWith('.nest.trait') === true ||
|
|
847
638
|
type.fullName.startsWith('.weave.') === true ||
|
|
848
639
|
type.fullName.startsWith('.google.trait.product.camera') === true))
|
|
@@ -854,17 +645,17 @@ export default class NestAccfactory {
|
|
|
854
645
|
}
|
|
855
646
|
|
|
856
647
|
if (firstRun === true) {
|
|
857
|
-
this?.log?.debug
|
|
648
|
+
this?.log?.debug?.('Starting Protobuf API trait observe for connection uuid "%s"', uuid);
|
|
858
649
|
}
|
|
859
650
|
|
|
860
651
|
fetchWrapper(
|
|
861
652
|
'post',
|
|
862
|
-
'https://' + this.#connections[
|
|
653
|
+
'https://' + this.#connections[uuid].protobufAPIHost + '/nestlabs.gateway.v2.GatewayService/Observe',
|
|
863
654
|
{
|
|
864
655
|
headers: {
|
|
865
|
-
referer: 'https://' + this.#connections[
|
|
656
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
866
657
|
'User-Agent': USERAGENT,
|
|
867
|
-
Authorization: 'Basic ' + this.#connections[
|
|
658
|
+
Authorization: 'Basic ' + this.#connections[uuid].token,
|
|
868
659
|
'Content-Type': 'application/x-protobuf',
|
|
869
660
|
'X-Accept-Content-Transfer-Encoding': 'binary',
|
|
870
661
|
'X-Accept-Response-Streaming': 'true',
|
|
@@ -929,13 +720,11 @@ export default class NestAccfactory {
|
|
|
929
720
|
}
|
|
930
721
|
|
|
931
722
|
// Send removed notice onto HomeKit device for it to process
|
|
932
|
-
|
|
933
|
-
this.#
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
);
|
|
938
|
-
}
|
|
723
|
+
this.#eventEmitter?.emit?.(
|
|
724
|
+
this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber].uuid,
|
|
725
|
+
HomeKitDevice.REMOVE,
|
|
726
|
+
{},
|
|
727
|
+
);
|
|
939
728
|
|
|
940
729
|
// Finally, remove from tracked devices
|
|
941
730
|
delete this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber];
|
|
@@ -955,8 +744,8 @@ export default class NestAccfactory {
|
|
|
955
744
|
decodedMessage.observeResponse[0].traitStates.map(async (trait) => {
|
|
956
745
|
if (typeof this.#rawData[trait.traitId.resourceId] === 'undefined') {
|
|
957
746
|
this.#rawData[trait.traitId.resourceId] = {};
|
|
958
|
-
this.#rawData[trait.traitId.resourceId].connection =
|
|
959
|
-
this.#rawData[trait.traitId.resourceId].source =
|
|
747
|
+
this.#rawData[trait.traitId.resourceId].connection = uuid;
|
|
748
|
+
this.#rawData[trait.traitId.resourceId].source = DATASOURCE.ProtobufAPI;
|
|
960
749
|
this.#rawData[trait.traitId.resourceId].value = {};
|
|
961
750
|
}
|
|
962
751
|
this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel] =
|
|
@@ -966,15 +755,15 @@ export default class NestAccfactory {
|
|
|
966
755
|
delete this.#rawData[trait.traitId.resourceId]['value'][trait.traitId.traitLabel]['@type'];
|
|
967
756
|
|
|
968
757
|
// If we have structure location details and associated geo-location details, get the weather data for the location
|
|
969
|
-
// We'll store this in the object key/value as per
|
|
758
|
+
// We'll store this in the object key/value as per Nest API
|
|
970
759
|
if (
|
|
971
760
|
trait.traitId.resourceId.startsWith('STRUCTURE_') === true &&
|
|
972
761
|
trait.traitId.traitLabel === 'structure_location' &&
|
|
973
762
|
isNaN(trait.patch.values?.geoCoordinate?.latitude) === false &&
|
|
974
763
|
isNaN(trait.patch.values?.geoCoordinate?.longitude) === false
|
|
975
764
|
) {
|
|
976
|
-
this.#rawData[trait.traitId.resourceId].value.weather = await this.#
|
|
977
|
-
|
|
765
|
+
this.#rawData[trait.traitId.resourceId].value.weather = await this.#getWeather(
|
|
766
|
+
uuid,
|
|
978
767
|
trait.traitId.resourceId,
|
|
979
768
|
Number(trait.patch.values.geoCoordinate.latitude),
|
|
980
769
|
Number(trait.patch.values.geoCoordinate.longitude),
|
|
@@ -989,15 +778,20 @@ export default class NestAccfactory {
|
|
|
989
778
|
}
|
|
990
779
|
})
|
|
991
780
|
.catch((error) => {
|
|
992
|
-
if (
|
|
993
|
-
|
|
994
|
-
|
|
781
|
+
if (
|
|
782
|
+
error?.cause === undefined ||
|
|
783
|
+
(typeof error.cause === 'object' && String(error.cause).toUpperCase().includes('TIMEOUT') === false)
|
|
784
|
+
) {
|
|
785
|
+
this?.log?.debug?.(
|
|
786
|
+
'Protobuf API had an error performing trait observe with connection uuid "%s". Error: "%s"',
|
|
787
|
+
uuid,
|
|
788
|
+
error?.message ?? String(error),
|
|
789
|
+
);
|
|
790
|
+
this?.log?.debug?.('Restarting Protobuf API trait observe for connection uuid "%s"', uuid);
|
|
995
791
|
}
|
|
996
792
|
})
|
|
997
|
-
|
|
998
793
|
.finally(() => {
|
|
999
|
-
|
|
1000
|
-
setTimeout(this.#subscribeProtobuf.bind(this, connectionUUID, false), 1000);
|
|
794
|
+
setTimeout(() => this.#subscribeProtobuf(uuid, false), 1000);
|
|
1001
795
|
});
|
|
1002
796
|
}
|
|
1003
797
|
|
|
@@ -1005,79 +799,44 @@ export default class NestAccfactory {
|
|
|
1005
799
|
Object.values(this.#processData('')).forEach((deviceData) => {
|
|
1006
800
|
if (this.#trackedDevices?.[deviceData?.serialNumber] === undefined && deviceData?.excluded === true) {
|
|
1007
801
|
// We haven't tracked this device before (ie: should be a new one) and but its excluded
|
|
1008
|
-
this?.log?.warn
|
|
802
|
+
this?.log?.warn?.('Device "%s" is ignored due to it being marked as excluded', deviceData.description);
|
|
1009
803
|
|
|
1010
804
|
// Track this device even though its excluded
|
|
1011
805
|
this.#trackedDevices[deviceData.serialNumber] = {
|
|
1012
|
-
uuid:
|
|
806
|
+
uuid: HomeKitDevice.generateUUID(HomeKitDevice.PLUGIN_NAME, this.api, deviceData.serialNumber),
|
|
1013
807
|
rawDataUuid: deviceData.nest_google_uuid,
|
|
1014
808
|
source: undefined,
|
|
809
|
+
timers: undefined,
|
|
1015
810
|
exclude: true,
|
|
1016
811
|
};
|
|
812
|
+
|
|
813
|
+
// If we're running under Homebridge, and the device is now marked as excluded and present in accessory cache
|
|
814
|
+
// Then we'll unregister it from the Homebridge platform
|
|
815
|
+
if (typeof this?.api?.unregisterPlatformAccessories === 'function') {
|
|
816
|
+
let accessory = this.cachedAccessories.find(
|
|
817
|
+
(accessory) => accessory?.UUID === this.#trackedDevices[deviceData.serialNumber].uuid,
|
|
818
|
+
);
|
|
819
|
+
if (accessory !== undefined && typeof accessory === 'object') {
|
|
820
|
+
this.api.unregisterPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [accessory]);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
1017
823
|
}
|
|
1018
824
|
|
|
1019
825
|
if (this.#trackedDevices?.[deviceData?.serialNumber] === undefined && deviceData?.excluded === false) {
|
|
1020
826
|
// We haven't tracked this device before (ie: should be a new one) and its not excluded
|
|
1021
827
|
// so create the required HomeKit accessories based upon the device data
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
if (deviceData.device_type === NestAccfactory.DeviceType.TEMPSENSOR && typeof NestTemperatureSensor === 'function') {
|
|
1035
|
-
// Nest Temperature Sensor - Categories.SENSOR = 10;
|
|
1036
|
-
let tempDevice = new NestTemperatureSensor(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
1037
|
-
tempDevice.add('Nest Temperature Sensor', 10, true);
|
|
1038
|
-
// Track this device once created
|
|
1039
|
-
this.#trackedDevices[deviceData.serialNumber] = {
|
|
1040
|
-
uuid: tempDevice.uuid,
|
|
1041
|
-
rawDataUuid: deviceData.nest_google_uuid,
|
|
1042
|
-
source: undefined,
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
if (deviceData.device_type === NestAccfactory.DeviceType.SMOKESENSOR && typeof NestProtect === 'function') {
|
|
1047
|
-
// Nest Protect(s) - Categories.SENSOR = 10
|
|
1048
|
-
let tempDevice = new NestProtect(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
1049
|
-
tempDevice.add('Nest Protect', 10, true);
|
|
1050
|
-
// Track this device once created
|
|
1051
|
-
this.#trackedDevices[deviceData.serialNumber] = {
|
|
1052
|
-
uuid: tempDevice.uuid,
|
|
1053
|
-
rawDataUuid: deviceData.nest_google_uuid,
|
|
1054
|
-
source: undefined,
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (
|
|
1059
|
-
(deviceData.device_type === NestAccfactory.DeviceType.CAMERA ||
|
|
1060
|
-
deviceData.device_type === NestAccfactory.DeviceType.DOORBELL ||
|
|
1061
|
-
deviceData.device_type === NestAccfactory.DeviceType.FLOODLIGHT) &&
|
|
1062
|
-
(typeof NestCamera === 'function' || typeof NestDoorbell === 'function' || typeof NestFloodlight === 'function')
|
|
1063
|
-
) {
|
|
1064
|
-
let accessoryName = 'Nest ' + deviceData.model.replace(/\s*(?:\([^()]*\))/gi, '');
|
|
1065
|
-
let tempDevice = undefined;
|
|
1066
|
-
if (deviceData.device_type === NestAccfactory.DeviceType.CAMERA) {
|
|
1067
|
-
// Nest Camera(s) - Categories.IP_CAMERA = 17
|
|
1068
|
-
tempDevice = new NestCamera(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
1069
|
-
tempDevice.add(accessoryName, 17, true);
|
|
1070
|
-
}
|
|
1071
|
-
if (deviceData.device_type === NestAccfactory.DeviceType.DOORBELL) {
|
|
1072
|
-
// Nest Doorbell(s) - Categories.VIDEO_DOORBELL = 18
|
|
1073
|
-
tempDevice = new NestDoorbell(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
1074
|
-
tempDevice.add(accessoryName, 18, true);
|
|
1075
|
-
}
|
|
1076
|
-
if (deviceData.device_type === NestAccfactory.DeviceType.FLOODLIGHT) {
|
|
1077
|
-
// Nest Camera(s) with Floodlight - Categories.IP_CAMERA = 17
|
|
1078
|
-
tempDevice = new NestFloodlight(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
1079
|
-
tempDevice.add(accessoryName, 17, true);
|
|
1080
|
-
}
|
|
828
|
+
let deviceClass = this.#deviceModules.get(deviceData.device_type);
|
|
829
|
+
if (deviceClass !== undefined) {
|
|
830
|
+
// We have found a device class for this device type, so we can create the device
|
|
831
|
+
let accessoryName =
|
|
832
|
+
(deviceData.manufacturer?.trim() || 'Nest') +
|
|
833
|
+
' ' +
|
|
834
|
+
deviceClass.TYPE.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
835
|
+
.replace(/[^a-zA-Z0-9 ]+/g, ' ')
|
|
836
|
+
.toLowerCase()
|
|
837
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
838
|
+
let tempDevice = new deviceClass(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
839
|
+
tempDevice.add(accessoryName, getDeviceHKCategory(deviceClass.TYPE), true);
|
|
1081
840
|
|
|
1082
841
|
// Track this device once created
|
|
1083
842
|
this.#trackedDevices[deviceData.serialNumber] = {
|
|
@@ -1085,254 +844,241 @@ export default class NestAccfactory {
|
|
|
1085
844
|
rawDataUuid: deviceData.nest_google_uuid,
|
|
1086
845
|
source: undefined,
|
|
1087
846
|
timers: {},
|
|
847
|
+
exclude: false,
|
|
1088
848
|
};
|
|
1089
849
|
|
|
1090
|
-
//
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
850
|
+
// Optional things for each device type
|
|
851
|
+
if (
|
|
852
|
+
deviceClass.TYPE === DeviceType.CAMERA ||
|
|
853
|
+
deviceClass.TYPE === DeviceType.DOORBELL ||
|
|
854
|
+
deviceClass.TYPE === DeviceType.FLOODLIGHT
|
|
855
|
+
) {
|
|
856
|
+
// Setup polling loop for camera/doorbell zone data
|
|
857
|
+
// This is only required for Nest API data sources as these details are present in Protobuf API
|
|
858
|
+
clearInterval(this.#trackedDevices?.[deviceData.serialNumber]?.timers?.zones);
|
|
859
|
+
this.#trackedDevices[deviceData.serialNumber].timers.zones = setInterval(async () => {
|
|
860
|
+
let nest_google_uuid = this.#trackedDevices?.[deviceData?.serialNumber]?.rawDataUuid;
|
|
861
|
+
if (
|
|
862
|
+
this.#rawData?.[nest_google_uuid]?.value !== undefined &&
|
|
863
|
+
this.#trackedDevices?.[deviceData?.serialNumber]?.source === DATASOURCE.NestAPI
|
|
864
|
+
) {
|
|
865
|
+
try {
|
|
866
|
+
let response = await fetchWrapper(
|
|
867
|
+
'get',
|
|
868
|
+
this.#rawData[nest_google_uuid].value.nexus_api_http_server_url +
|
|
869
|
+
'/cuepoint_category/' +
|
|
870
|
+
nest_google_uuid.split('.')[1],
|
|
871
|
+
{
|
|
872
|
+
headers: {
|
|
873
|
+
referer: 'https://' + this.#connections[this.#rawData[nest_google_uuid].connection].referer,
|
|
874
|
+
'User-Agent': USERAGENT,
|
|
875
|
+
[this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.key]:
|
|
876
|
+
this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.value +
|
|
877
|
+
this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.token,
|
|
878
|
+
},
|
|
879
|
+
timeout: CAMERAZONEPOLLING,
|
|
880
|
+
},
|
|
881
|
+
);
|
|
882
|
+
let data = await response.json();
|
|
883
|
+
|
|
884
|
+
// Transform activity zones if present
|
|
885
|
+
let zones =
|
|
886
|
+
Array.isArray(data) === true
|
|
887
|
+
? data
|
|
888
|
+
.filter((zone) => zone.type.toUpperCase() === 'ACTIVITY' || zone.type.toUpperCase() === 'REGION')
|
|
889
|
+
.map((zone) => ({
|
|
890
|
+
id: zone.id === 0 ? 1 : zone.id,
|
|
891
|
+
name: makeHomeKitName(zone.label),
|
|
892
|
+
hidden: zone.hidden === true,
|
|
893
|
+
uri: zone.nexusapi_image_uri,
|
|
894
|
+
}))
|
|
895
|
+
: [];
|
|
1125
896
|
|
|
1126
897
|
// Update internal structure with new zone details.
|
|
1127
|
-
// We do a test to see if
|
|
898
|
+
// We do a test to see if it's still present, not interval loop not finished or device removed
|
|
1128
899
|
if (this.#rawData?.[nest_google_uuid]?.value !== undefined) {
|
|
1129
900
|
this.#rawData[nest_google_uuid].value.activity_zones = zones;
|
|
1130
901
|
|
|
1131
902
|
// Send updated data onto HomeKit device for it to process
|
|
1132
|
-
|
|
1133
|
-
this.#eventEmitter
|
|
903
|
+
this.#trackedDevices?.[deviceData?.serialNumber]?.uuid &&
|
|
904
|
+
this.#eventEmitter?.emit?.(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, {
|
|
1134
905
|
activity_zones: zones,
|
|
1135
906
|
});
|
|
1136
|
-
}
|
|
1137
907
|
}
|
|
1138
|
-
})
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
|
|
1144
|
-
this?.log?.debug
|
|
1145
|
-
) {
|
|
1146
|
-
this.log.debug(
|
|
1147
|
-
'REST API had error retrieving camera/doorbell activity zones for "%s". Error was "%s"',
|
|
908
|
+
} catch (error) {
|
|
909
|
+
// Log debug message if it wasn't a timeout
|
|
910
|
+
if (error?.cause !== undefined && String(error.cause).toUpperCase().includes('TIMEOUT') === false) {
|
|
911
|
+
this?.log?.debug?.(
|
|
912
|
+
'Nest API had error retrieving camera/doorbell activity zones for "%s". Error was "%s"',
|
|
1148
913
|
deviceData.description,
|
|
1149
914
|
error?.code,
|
|
1150
915
|
);
|
|
1151
916
|
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}, CAMERAZONEPOLLING);
|
|
1155
920
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
resourceCommands: [
|
|
921
|
+
// Setup polling loop for camera/doorbell alert data, clearing any existing polling loop
|
|
922
|
+
clearInterval(this.#trackedDevices?.[deviceData.serialNumber]?.timers?.alerts);
|
|
923
|
+
this.#trackedDevices[deviceData.serialNumber].timers.alerts = setInterval(async () => {
|
|
924
|
+
let alerts = []; // No alerts to processed yet
|
|
925
|
+
let nest_google_uuid = this.#trackedDevices?.[deviceData?.serialNumber]?.rawDataUuid;
|
|
926
|
+
if (
|
|
927
|
+
this.#rawData?.[nest_google_uuid]?.value !== undefined &&
|
|
928
|
+
this.#trackedDevices?.[deviceData?.serialNumber]?.source === DATASOURCE.ProtobufAPI
|
|
929
|
+
) {
|
|
930
|
+
let commandResponse = await this.#protobufCommand(
|
|
931
|
+
this.#rawData[nest_google_uuid].connection,
|
|
932
|
+
'ResourceApi',
|
|
933
|
+
'SendCommand',
|
|
1170
934
|
{
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
935
|
+
resourceRequest: {
|
|
936
|
+
resourceId: nest_google_uuid,
|
|
937
|
+
requestId: crypto.randomUUID(),
|
|
938
|
+
},
|
|
939
|
+
resourceCommands: [
|
|
940
|
+
{
|
|
941
|
+
traitLabel: 'camera_observation_history',
|
|
942
|
+
command: {
|
|
943
|
+
type_url: 'type.nestlabs.com/nest.trait.history.CameraObservationHistoryTrait.CameraObservationHistoryRequest',
|
|
944
|
+
value: {
|
|
945
|
+
// We want camera history from now for upto 30secs from now
|
|
946
|
+
queryStartTime: { seconds: Math.floor(Date.now() / 1000), nanos: (Math.round(Date.now()) % 1000) * 1e6 },
|
|
947
|
+
queryEndTime: {
|
|
948
|
+
seconds: Math.floor((Date.now() + 30000) / 1000),
|
|
949
|
+
nanos: (Math.round(Date.now() + 30000) % 1000) * 1e6,
|
|
950
|
+
},
|
|
951
|
+
},
|
|
1180
952
|
},
|
|
1181
953
|
},
|
|
1182
|
-
|
|
954
|
+
],
|
|
1183
955
|
},
|
|
1184
|
-
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
if (
|
|
1188
|
-
typeof commandResponse?.sendCommandResponse?.[0]?.traitOperations?.[0]?.event?.event?.cameraEventWindow?.cameraEvent ===
|
|
1189
|
-
'object'
|
|
1190
|
-
) {
|
|
1191
|
-
commandResponse.sendCommandResponse[0].traitOperations[0].event.event.cameraEventWindow.cameraEvent.forEach((event) => {
|
|
1192
|
-
alerts.push({
|
|
1193
|
-
playback_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
|
|
1194
|
-
start_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
|
|
1195
|
-
end_time: parseInt(event.endTime.seconds) * 1000 + parseInt(event.endTime.nanos) / 1000000,
|
|
1196
|
-
id: event.eventId,
|
|
1197
|
-
zone_ids:
|
|
1198
|
-
typeof event.activityZone === 'object'
|
|
1199
|
-
? event.activityZone.map((zone) => (zone?.zoneIndex !== undefined ? zone.zoneIndex : zone.internalIndex))
|
|
1200
|
-
: [],
|
|
1201
|
-
types: event.eventType
|
|
1202
|
-
.map((event) => (event.startsWith('EVENT_') === true ? event.split('EVENT_')[1].toLowerCase() : ''))
|
|
1203
|
-
.filter((event) => event),
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
// Fix up event types to match REST API
|
|
1207
|
-
// 'EVENT_UNFAMILIAR_FACE' = 'unfamiliar-face'
|
|
1208
|
-
// 'EVENT_PERSON_TALKING' = 'personHeard'
|
|
1209
|
-
// 'EVENT_DOG_BARKING' = 'dogBarking'
|
|
1210
|
-
// <---- TODO (as the ones we use match from Protobuf)
|
|
1211
|
-
});
|
|
956
|
+
);
|
|
1212
957
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
958
|
+
if (
|
|
959
|
+
Array.isArray(
|
|
960
|
+
commandResponse?.sendCommandResponse?.[0]?.traitOperations?.[0]?.event?.event?.cameraEventWindow?.cameraEvent,
|
|
961
|
+
) === true
|
|
962
|
+
) {
|
|
963
|
+
alerts = commandResponse.sendCommandResponse[0].traitOperations[0].event.event.cameraEventWindow.cameraEvent
|
|
964
|
+
.map((event) => ({
|
|
965
|
+
playback_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
|
|
966
|
+
start_time: parseInt(event.startTime.seconds) * 1000 + parseInt(event.startTime.nanos) / 1000000,
|
|
967
|
+
end_time: parseInt(event.endTime.seconds) * 1000 + parseInt(event.endTime.nanos) / 1000000,
|
|
968
|
+
id: event.eventId,
|
|
969
|
+
zone_ids:
|
|
970
|
+
typeof event.activityZone === 'object'
|
|
971
|
+
? event.activityZone.map((zone) => (zone?.zoneIndex !== undefined ? zone.zoneIndex : zone.internalIndex))
|
|
972
|
+
: [],
|
|
973
|
+
types: event.eventType
|
|
974
|
+
.map((event) => {
|
|
975
|
+
if (event === 'EVENT_UNFAMILIAR_FACE') {
|
|
976
|
+
return 'unfamiliar-face';
|
|
977
|
+
}
|
|
978
|
+
if (event === 'EVENT_PERSON_TALKING') {
|
|
979
|
+
return 'personHeard';
|
|
980
|
+
}
|
|
981
|
+
if (event === 'EVENT_DOG_BARKING') {
|
|
982
|
+
return 'dogBarking';
|
|
983
|
+
}
|
|
984
|
+
return event.startsWith('EVENT_') ? event.split('EVENT_')[1].toLowerCase() : '';
|
|
985
|
+
})
|
|
986
|
+
.filter((event) => event),
|
|
987
|
+
}))
|
|
988
|
+
.sort((a, b) => b.start_time - a.start_time);
|
|
989
|
+
}
|
|
1219
990
|
}
|
|
1220
|
-
}
|
|
1221
991
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
.then((data) => {
|
|
1247
|
-
data.forEach((alert) => {
|
|
1248
|
-
// Fix up alert zone IDs. If there is an ID of 0, we'll transform to 1. ie: main zone
|
|
1249
|
-
// If there are NO zone IDs, we'll put a 1 in there ie: main zone
|
|
1250
|
-
alert.zone_ids = alert.zone_ids.map((id) => (id !== 0 ? id : 1));
|
|
1251
|
-
if (alert.zone_ids.length === 0) {
|
|
1252
|
-
alert.zone_ids.push(1);
|
|
1253
|
-
}
|
|
1254
|
-
alerts.push({
|
|
1255
|
-
playback_time: alert.playback_time,
|
|
1256
|
-
start_time: alert.start_time,
|
|
1257
|
-
end_time: alert.end_time,
|
|
1258
|
-
id: alert.id,
|
|
1259
|
-
zone_ids: alert.zone_ids,
|
|
1260
|
-
types: alert.types,
|
|
1261
|
-
});
|
|
1262
|
-
});
|
|
992
|
+
if (
|
|
993
|
+
this.#rawData?.[nest_google_uuid]?.value !== undefined &&
|
|
994
|
+
this.#trackedDevices?.[deviceData?.serialNumber]?.source === DATASOURCE.NestAPI
|
|
995
|
+
) {
|
|
996
|
+
try {
|
|
997
|
+
let response = await fetchWrapper(
|
|
998
|
+
'get',
|
|
999
|
+
this.#rawData[nest_google_uuid].value.nexus_api_http_server_url +
|
|
1000
|
+
'/cuepoint/' +
|
|
1001
|
+
nest_google_uuid.split('.')[1] +
|
|
1002
|
+
'/2?start_time=' +
|
|
1003
|
+
Math.floor(Date.now() / 1000 - 30),
|
|
1004
|
+
{
|
|
1005
|
+
headers: {
|
|
1006
|
+
referer: 'https://' + this.#connections[this.#rawData[nest_google_uuid].connection].referer,
|
|
1007
|
+
'User-Agent': USERAGENT,
|
|
1008
|
+
[this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.key]:
|
|
1009
|
+
this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.value +
|
|
1010
|
+
this.#connections[this.#rawData[nest_google_uuid].connection].cameraAPI.token,
|
|
1011
|
+
},
|
|
1012
|
+
timeout: CAMERAALERTPOLLING,
|
|
1013
|
+
retry: 3,
|
|
1014
|
+
},
|
|
1015
|
+
);
|
|
1263
1016
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1017
|
+
let data = await response.json();
|
|
1018
|
+
|
|
1019
|
+
alerts =
|
|
1020
|
+
Array.isArray(data) === true
|
|
1021
|
+
? data
|
|
1022
|
+
.map((alert) => {
|
|
1023
|
+
alert.zone_ids = alert.zone_ids.map((id) => (id !== 0 ? id : 1));
|
|
1024
|
+
if (alert.zone_ids.length === 0) {
|
|
1025
|
+
alert.zone_ids.push(1);
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
playback_time: alert.playback_time,
|
|
1029
|
+
start_time: alert.start_time,
|
|
1030
|
+
end_time: alert.end_time,
|
|
1031
|
+
id: alert.id,
|
|
1032
|
+
zone_ids: alert.zone_ids,
|
|
1033
|
+
types: alert.types,
|
|
1034
|
+
};
|
|
1035
|
+
})
|
|
1036
|
+
.sort((a, b) => b.start_time - a.start_time)
|
|
1037
|
+
: [];
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
if (error?.cause !== undefined && String(error.cause).toUpperCase().includes('TIMEOUT') === false) {
|
|
1040
|
+
this?.log?.debug?.(
|
|
1041
|
+
'Nest API had error retrieving camera/doorbell activity notifications for "%s". Error was "%s"',
|
|
1280
1042
|
deviceData.description,
|
|
1281
1043
|
error?.code,
|
|
1282
1044
|
);
|
|
1283
1045
|
}
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// Update internal structure with new alerts.
|
|
1288
|
-
// We do a test to see if its still present not interval loop not finished or device removed
|
|
1289
|
-
if (this.#rawData?.[nest_google_uuid]?.value !== undefined) {
|
|
1290
|
-
this.#rawData[nest_google_uuid].value.alerts = alerts;
|
|
1291
|
-
|
|
1292
|
-
// Send updated alerts onto HomeKit device for it to process
|
|
1293
|
-
if (this.#eventEmitter !== undefined && this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
|
|
1294
|
-
this.#eventEmitter.emit(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, {
|
|
1295
|
-
alerts: alerts,
|
|
1296
|
-
});
|
|
1046
|
+
}
|
|
1297
1047
|
}
|
|
1298
|
-
}
|
|
1299
|
-
}, CAMERAALERTPOLLING);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
if (deviceData.device_type === NestAccfactory.DeviceType.WEATHER && typeof NestWeather === 'function') {
|
|
1303
|
-
// Nest 'Virtual' weather station - Categories.SENSOR = 10
|
|
1304
|
-
let tempDevice = new NestWeather(this.cachedAccessories, this.api, this.log, this.#eventEmitter, deviceData);
|
|
1305
|
-
tempDevice.add('Nest Weather', 10, true);
|
|
1306
1048
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
source: undefined,
|
|
1312
|
-
timers: {},
|
|
1313
|
-
};
|
|
1049
|
+
// Update internal structure with new alerts.
|
|
1050
|
+
// We do a test to see if its still present not interval loop not finished or device removed
|
|
1051
|
+
if (this.#rawData?.[nest_google_uuid]?.value !== undefined) {
|
|
1052
|
+
this.#rawData[nest_google_uuid].value.alerts = alerts;
|
|
1314
1053
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
nest_google_uuid,
|
|
1322
|
-
this.#rawData[nest_google_uuid].value.weather.latitude,
|
|
1323
|
-
this.#rawData[nest_google_uuid].value.weather.longitude,
|
|
1324
|
-
);
|
|
1054
|
+
// Send updated alerts onto HomeKit device for it to process
|
|
1055
|
+
this.#trackedDevices?.[deviceData?.serialNumber]?.uuid &&
|
|
1056
|
+
this.#eventEmitter?.emit?.(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, { alerts: alerts });
|
|
1057
|
+
}
|
|
1058
|
+
}, CAMERAALERTPOLLING);
|
|
1059
|
+
}
|
|
1325
1060
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1061
|
+
if (deviceClass.TYPE === DeviceType.WEATHER) {
|
|
1062
|
+
// Setup polling loop for weather data, clearing any existing polling loop
|
|
1063
|
+
clearInterval(this.#trackedDevices?.[deviceData.serialNumber]?.timers?.weather);
|
|
1064
|
+
this.#trackedDevices[deviceData.serialNumber].timers.weather = setInterval(async () => {
|
|
1065
|
+
if (this.#rawData?.[this.#trackedDevices?.[deviceData.serialNumber]?.rawDataUuid] !== undefined) {
|
|
1066
|
+
this.#rawData[this.#trackedDevices[deviceData.serialNumber].rawDataUuid].value.weather = await this.#getWeather(
|
|
1067
|
+
this.#rawData[this.#trackedDevices[deviceData.serialNumber].rawDataUuid].connection,
|
|
1068
|
+
this.#trackedDevices[deviceData.serialNumber].rawDataUuid,
|
|
1069
|
+
this.#rawData[this.#trackedDevices[deviceData.serialNumber].rawDataUuid].value.weather.latitude,
|
|
1070
|
+
this.#rawData[this.#trackedDevices[deviceData.serialNumber].rawDataUuid].value.weather.longitude,
|
|
1332
1071
|
);
|
|
1072
|
+
|
|
1073
|
+
this.#trackedDevices?.[deviceData.serialNumber]?.uuid &&
|
|
1074
|
+
this.#eventEmitter?.emit?.(
|
|
1075
|
+
this.#trackedDevices[deviceData.serialNumber].uuid,
|
|
1076
|
+
HomeKitDevice.UPDATE,
|
|
1077
|
+
this.#processData(this.#trackedDevices[deviceData.serialNumber].rawDataUuid)?.[deviceData.serialNumber],
|
|
1078
|
+
);
|
|
1333
1079
|
}
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1080
|
+
}, WEATHERPOLLING);
|
|
1081
|
+
}
|
|
1336
1082
|
}
|
|
1337
1083
|
}
|
|
1338
1084
|
|
|
@@ -1343,21 +1089,19 @@ export default class NestAccfactory {
|
|
|
1343
1089
|
this.#rawData[deviceData.nest_google_uuid].source !== this.#trackedDevices[deviceData.serialNumber].source
|
|
1344
1090
|
) {
|
|
1345
1091
|
// Data source for this device has been updated
|
|
1346
|
-
this?.log?.debug
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
);
|
|
1092
|
+
this?.log?.debug?.(
|
|
1093
|
+
'Using %s API as data source for "%s" from connection uuid "%s"',
|
|
1094
|
+
this.#rawData[deviceData.nest_google_uuid]?.source,
|
|
1095
|
+
deviceData.description,
|
|
1096
|
+
this.#rawData[deviceData.nest_google_uuid].connection,
|
|
1097
|
+
);
|
|
1353
1098
|
|
|
1354
1099
|
this.#trackedDevices[deviceData.serialNumber].source = this.#rawData[deviceData.nest_google_uuid].source;
|
|
1355
1100
|
this.#trackedDevices[deviceData.serialNumber].rawDataUuid = deviceData.nest_google_uuid;
|
|
1356
1101
|
}
|
|
1357
1102
|
|
|
1358
|
-
|
|
1359
|
-
this.#eventEmitter
|
|
1360
|
-
}
|
|
1103
|
+
this.#trackedDevices?.[deviceData?.serialNumber]?.uuid &&
|
|
1104
|
+
this.#eventEmitter?.emit?.(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, deviceData);
|
|
1361
1105
|
}
|
|
1362
1106
|
});
|
|
1363
1107
|
}
|
|
@@ -1368,37 +1112,69 @@ export default class NestAccfactory {
|
|
|
1368
1112
|
}
|
|
1369
1113
|
let devices = {};
|
|
1370
1114
|
|
|
1371
|
-
// Get the device(s) location from
|
|
1372
|
-
// We'll test in both
|
|
1115
|
+
// Get the device(s) location from structure
|
|
1116
|
+
// We'll test in both Nest and Protobuf API data
|
|
1373
1117
|
const get_location_name = (structure_id, where_id) => {
|
|
1374
1118
|
let location = '';
|
|
1375
1119
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
this.#rawData['where.' + structure_id]
|
|
1379
|
-
|
|
1380
|
-
|
|
1120
|
+
if (typeof structure_id === 'string' && typeof where_id === 'string') {
|
|
1121
|
+
// Check Nest data
|
|
1122
|
+
if (typeof this.#rawData?.['where.' + structure_id]?.value === 'object') {
|
|
1123
|
+
this.#rawData['where.' + structure_id].value.wheres.forEach((value) => {
|
|
1124
|
+
if (where_id === value.where_id) {
|
|
1125
|
+
location = value.name;
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Check Protobuf data (combined predefined and custom)
|
|
1131
|
+
let protobufWheres = [
|
|
1132
|
+
...Object.values(this.#rawData?.[structure_id]?.value?.located_annotations?.predefinedWheres || {}),
|
|
1133
|
+
...Object.values(this.#rawData?.[structure_id]?.value?.located_annotations?.customWheres || {}),
|
|
1134
|
+
];
|
|
1135
|
+
|
|
1136
|
+
protobufWheres.forEach((value) => {
|
|
1137
|
+
if (value?.whereId?.resourceId === where_id) {
|
|
1138
|
+
location = value.label?.literal;
|
|
1381
1139
|
}
|
|
1382
1140
|
});
|
|
1383
1141
|
}
|
|
1142
|
+
return location;
|
|
1143
|
+
};
|
|
1384
1144
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1145
|
+
// Process software version strings and return as x.x.x
|
|
1146
|
+
// handles things like:
|
|
1147
|
+
// 1.0a17 -> 1.0.17
|
|
1148
|
+
// 3.6rc8 -> 3.6.8
|
|
1149
|
+
// rquartz-user 1 OPENMASTER 507800056 test-keys stable-channel stable-channel -> 507800056
|
|
1150
|
+
// nq-user 1.73 OPENMASTER 422270 release-keys stable-channel stable-channel -> 422270
|
|
1151
|
+
const process_software_version = (versionString) => {
|
|
1152
|
+
let version = '0.0.0';
|
|
1153
|
+
if (typeof versionString === 'string') {
|
|
1154
|
+
let normalised = versionString.replace(/[-_]/g, '.');
|
|
1155
|
+
let tokens = normalised.split(/\s+/);
|
|
1156
|
+
let candidate = tokens[3] || normalised;
|
|
1157
|
+
let match = candidate.match(/\d+(?:\.\d+)*[a-zA-Z]*\d*/) || normalised.match(/\d+(?:\.\d+)*[a-zA-Z]*\d*/);
|
|
1158
|
+
|
|
1159
|
+
if (Array.isArray(match)) {
|
|
1160
|
+
let raw = match[0];
|
|
1161
|
+
if (raw.includes('.') === false) {
|
|
1162
|
+
return raw; // Return single-number version like "422270" as-is
|
|
1390
1163
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1164
|
+
|
|
1165
|
+
let parts = raw.split('.').flatMap((part) => {
|
|
1166
|
+
let [, n1, , n2] = part.match(/^(\d+)([a-zA-Z]+)?(\d+)?$/) || [];
|
|
1167
|
+
return [n1, n2].filter(Boolean).map(Number);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
while (parts.length < 3) {
|
|
1171
|
+
parts.push(0);
|
|
1397
1172
|
}
|
|
1398
|
-
|
|
1173
|
+
version = parts.slice(0, 3).join('.');
|
|
1174
|
+
}
|
|
1399
1175
|
}
|
|
1400
1176
|
|
|
1401
|
-
return
|
|
1177
|
+
return version;
|
|
1402
1178
|
};
|
|
1403
1179
|
|
|
1404
1180
|
// Process common data for all devices
|
|
@@ -1406,38 +1182,43 @@ export default class NestAccfactory {
|
|
|
1406
1182
|
let processed = {};
|
|
1407
1183
|
try {
|
|
1408
1184
|
// Fix up data we need to
|
|
1185
|
+
let deviceOptions = this.config?.devices?.find(
|
|
1186
|
+
(device) => device?.serialNumber?.toUpperCase?.() === data?.serialNumber?.toUpperCase?.(),
|
|
1187
|
+
);
|
|
1409
1188
|
data.nest_google_uuid = object_key;
|
|
1410
1189
|
data.serialNumber = data.serialNumber.toUpperCase(); // ensure serial numbers are in upper case
|
|
1411
|
-
data.excluded = this
|
|
1412
|
-
data.manufacturer = typeof data?.manufacturer === 'string' ? data.manufacturer : 'Nest';
|
|
1413
|
-
data.softwareVersion =
|
|
1414
|
-
typeof data?.softwareVersion === 'string' ? data.softwareVersion.replace(/([a-zA-Z]+)/g, '.').replace(/-/g, '.') : '0.0.0';
|
|
1190
|
+
data.excluded = this?.config?.options?.exclude === true ? deviceOptions?.exclude !== false : deviceOptions?.exclude === true;
|
|
1191
|
+
data.manufacturer = typeof data?.manufacturer === 'string' && data.manufacturer !== '' ? data.manufacturer : 'Nest';
|
|
1192
|
+
data.softwareVersion = process_software_version(data.softwareVersion);
|
|
1415
1193
|
let description = typeof data?.description === 'string' ? data.description : '';
|
|
1416
1194
|
let location = typeof data?.location === 'string' ? data.location : '';
|
|
1417
|
-
if (description === '') {
|
|
1195
|
+
if (description === '' && location !== '') {
|
|
1418
1196
|
description = location;
|
|
1419
1197
|
location = '';
|
|
1420
1198
|
}
|
|
1199
|
+
if (description === '' && location === '') {
|
|
1200
|
+
description = 'unknown description';
|
|
1201
|
+
}
|
|
1421
1202
|
data.description = makeHomeKitName(location === '' ? description : description + ' - ' + location);
|
|
1422
1203
|
delete data.location;
|
|
1423
1204
|
|
|
1424
1205
|
// Insert HomeKit pairing code for when using HAP-NodeJS library rather than Homebridge
|
|
1425
1206
|
// Validate the pairing code is in the format of "xxx-xx-xxx" or "xxxx-xxxx"
|
|
1426
1207
|
if (
|
|
1427
|
-
|
|
1428
|
-
|
|
1208
|
+
typeof this.config?.options?.hkPairingCode === 'string' &&
|
|
1209
|
+
(HomeKitDevice.HK_PIN_3_2_3.test(this.config.options.hkPairingCode) ||
|
|
1210
|
+
HomeKitDevice.HK_PIN_4_4.test(this.config.options.hkPairingCode))
|
|
1429
1211
|
) {
|
|
1430
1212
|
data.hkPairingCode = this.config.options.hkPairingCode;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
new RegExp(/^([0-9]{4}-[0-9]{4})$/).test(this.config?.devices?.[data.serialNumber]?.hkPairingCode) === true
|
|
1213
|
+
} else if (
|
|
1214
|
+
typeof deviceOptions?.hkPairingCode === 'string' &&
|
|
1215
|
+
(HomeKitDevice.HK_PIN_3_2_3.test(deviceOptions.hkPairingCode) || HomeKitDevice.HK_PIN_4_4.test(deviceOptions.hkPairingCode))
|
|
1435
1216
|
) {
|
|
1436
|
-
data.hkPairingCode =
|
|
1217
|
+
data.hkPairingCode = deviceOptions.hkPairingCode;
|
|
1437
1218
|
}
|
|
1438
1219
|
|
|
1439
1220
|
// If we have a hkPairingCode defined, we need to generate a hkUsername also
|
|
1440
|
-
if (data
|
|
1221
|
+
if (data?.hkPairingCode !== undefined) {
|
|
1441
1222
|
// Use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off serial number for last 6 digits.
|
|
1442
1223
|
data.hkUsername = ('18B430' + crc24(data.serialNumber.toUpperCase()))
|
|
1443
1224
|
.toString('hex')
|
|
@@ -1459,14 +1240,19 @@ export default class NestAccfactory {
|
|
|
1459
1240
|
let processed = {};
|
|
1460
1241
|
try {
|
|
1461
1242
|
// Fix up data we need to
|
|
1243
|
+
data.device_type = DeviceType.THERMOSTAT; // Nest Thermostat
|
|
1244
|
+
|
|
1245
|
+
// If we have hot water control, it should be a 'UK/EU' model, so add that after the 'gen' tag in the model name
|
|
1246
|
+
data.model = data.has_hot_water_control === true ? data.model.replace(/\bgen\)/, 'gen, EU)') : data.model;
|
|
1247
|
+
|
|
1462
1248
|
data = process_common_data(object_key, data);
|
|
1463
|
-
data.device_type = NestAccfactory.DeviceType.THERMOSTAT; // Nest Thermostat
|
|
1464
1249
|
data.target_temperature_high = adjustTemperature(data.target_temperature_high, 'C', 'C', true);
|
|
1465
1250
|
data.target_temperature_low = adjustTemperature(data.target_temperature_low, 'C', 'C', true);
|
|
1466
1251
|
data.target_temperature = adjustTemperature(data.target_temperature, 'C', 'C', true);
|
|
1467
1252
|
data.backplate_temperature = adjustTemperature(data.backplate_temperature, 'C', 'C', true);
|
|
1468
1253
|
data.current_temperature = adjustTemperature(data.current_temperature, 'C', 'C', true);
|
|
1469
1254
|
data.battery_level = scaleValue(data.battery_level, 3.6, 3.9, 0, 100);
|
|
1255
|
+
|
|
1470
1256
|
processed = data;
|
|
1471
1257
|
// eslint-disable-next-line no-unused-vars
|
|
1472
1258
|
} catch (error) {
|
|
@@ -1476,6 +1262,10 @@ export default class NestAccfactory {
|
|
|
1476
1262
|
};
|
|
1477
1263
|
|
|
1478
1264
|
const PROTOBUF_THERMOSTAT_RESOURCES = [
|
|
1265
|
+
'nest.resource.NestAmber1DisplayResource',
|
|
1266
|
+
'nest.resource.NestAmber2DisplayResource',
|
|
1267
|
+
'nest.resource.NestLearningThermostat1Resource',
|
|
1268
|
+
'nest.resource.NestLearningThermostat2Resource',
|
|
1479
1269
|
'nest.resource.NestLearningThermostat3Resource',
|
|
1480
1270
|
'nest.resource.NestAgateDisplayResource',
|
|
1481
1271
|
'nest.resource.NestOnyxResource',
|
|
@@ -1493,50 +1283,55 @@ export default class NestAccfactory {
|
|
|
1493
1283
|
let tempDevice = {};
|
|
1494
1284
|
try {
|
|
1495
1285
|
if (
|
|
1496
|
-
value?.source ===
|
|
1497
|
-
this.config.options?.
|
|
1286
|
+
value?.source === DATASOURCE.ProtobufAPI &&
|
|
1287
|
+
this.config.options?.useGoogleAPI === true &&
|
|
1498
1288
|
value.value?.configuration_done?.deviceReady === true
|
|
1499
1289
|
) {
|
|
1500
1290
|
let RESTTypeData = {};
|
|
1501
1291
|
RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
|
|
1502
|
-
RESTTypeData.softwareVersion =
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
if (value.value.device_info.typeName === 'nest.resource.
|
|
1292
|
+
RESTTypeData.softwareVersion = value.value.device_identity.softwareVersion;
|
|
1293
|
+
RESTTypeData.model = 'Thermostat (unknown)';
|
|
1294
|
+
if (value.value.device_info.typeName === 'nest.resource.NestLearningThermostat1Resource') {
|
|
1295
|
+
RESTTypeData.model = 'Learning Thermostat (1st gen)';
|
|
1296
|
+
}
|
|
1297
|
+
if (value.value.device_info.typeName === 'nest.resource.NestLearningThermostat2Resource') {
|
|
1298
|
+
RESTTypeData.model = 'Learning Thermostat (2nd gen)';
|
|
1299
|
+
}
|
|
1300
|
+
if (
|
|
1301
|
+
value.value.device_info.typeName === 'nest.resource.NestLearningThermostat3Resource' ||
|
|
1302
|
+
value.value.device_info.typeName === 'nest.resource.NestAmber2DisplayResource'
|
|
1303
|
+
) {
|
|
1508
1304
|
RESTTypeData.model = 'Learning Thermostat (3rd gen)';
|
|
1509
1305
|
}
|
|
1510
1306
|
if (value.value.device_info.typeName === 'google.resource.GoogleBismuth1Resource') {
|
|
1511
1307
|
RESTTypeData.model = 'Learning Thermostat (4th gen)';
|
|
1512
1308
|
}
|
|
1513
|
-
if (
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1309
|
+
if (
|
|
1310
|
+
value.value.device_info.typeName === 'nest.resource.NestOnyxResource' ||
|
|
1311
|
+
value.value.device_info.typeName === 'nest.resource.NestAgateDisplayResource'
|
|
1312
|
+
) {
|
|
1517
1313
|
RESTTypeData.model = 'Thermostat E (1st gen)';
|
|
1518
1314
|
}
|
|
1519
1315
|
if (value.value.device_info.typeName === 'google.resource.GoogleZirconium1Resource') {
|
|
1520
|
-
RESTTypeData.model = 'Thermostat (2020
|
|
1316
|
+
RESTTypeData.model = 'Thermostat (2020)';
|
|
1521
1317
|
}
|
|
1522
1318
|
RESTTypeData.current_humidity =
|
|
1523
|
-
isNaN(value.value
|
|
1319
|
+
isNaN(value.value?.current_humidity?.humidityValue?.humidity?.value) === false
|
|
1524
1320
|
? Number(value.value.current_humidity.humidityValue.humidity.value)
|
|
1525
1321
|
: 0.0;
|
|
1526
|
-
RESTTypeData.temperature_scale = value.value
|
|
1527
|
-
RESTTypeData.removed_from_base =
|
|
1322
|
+
RESTTypeData.temperature_scale = value.value?.display_settings?.temperatureScale === 'TEMPERATURE_SCALE_F' ? 'F' : 'C';
|
|
1323
|
+
RESTTypeData.removed_from_base =
|
|
1324
|
+
Array.isArray(value.value?.display?.thermostatState) === true && value.value?.display.thermostatState.includes('bpd');
|
|
1528
1325
|
RESTTypeData.backplate_temperature = parseFloat(value.value.backplate_temperature.temperatureValue.temperature.value);
|
|
1529
1326
|
RESTTypeData.current_temperature = parseFloat(value.value.current_temperature.temperatureValue.temperature.value);
|
|
1530
1327
|
RESTTypeData.battery_level = parseFloat(value.value.battery_voltage.batteryValue.batteryVoltage.value);
|
|
1531
1328
|
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
1532
1329
|
RESTTypeData.leaf = value.value?.leaf?.active === true;
|
|
1533
|
-
RESTTypeData.has_humidifier = value.value
|
|
1534
|
-
RESTTypeData.has_dehumidifier = value.value
|
|
1330
|
+
RESTTypeData.has_humidifier = value.value?.hvac_equipment_capabilities?.hasHumidifier === true;
|
|
1331
|
+
RESTTypeData.has_dehumidifier = value.value?.hvac_equipment_capabilities?.hasDehumidifier === true;
|
|
1535
1332
|
RESTTypeData.has_fan =
|
|
1536
|
-
typeof value.value
|
|
1537
|
-
value.value.fan_control_capabilities.maxAvailableSpeed !== 'FAN_SPEED_SETTING_OFF'
|
|
1538
|
-
? true
|
|
1539
|
-
: false;
|
|
1333
|
+
typeof value.value?.fan_control_capabilities?.maxAvailableSpeed === 'string' &&
|
|
1334
|
+
value.value.fan_control_capabilities.maxAvailableSpeed !== 'FAN_SPEED_SETTING_OFF';
|
|
1540
1335
|
RESTTypeData.can_cool =
|
|
1541
1336
|
value.value?.hvac_equipment_capabilities?.hasStage1Cool === true ||
|
|
1542
1337
|
value.value?.hvac_equipment_capabilities?.hasStage2Cool === true ||
|
|
@@ -1641,7 +1436,9 @@ export default class NestAccfactory {
|
|
|
1641
1436
|
}
|
|
1642
1437
|
|
|
1643
1438
|
// Update fan status, on or off and max number of speeds supported
|
|
1644
|
-
RESTTypeData.fan_state =
|
|
1439
|
+
RESTTypeData.fan_state =
|
|
1440
|
+
isNaN(value.value?.fan_control_settings?.timerEnd?.seconds) === false &&
|
|
1441
|
+
Number(value.value.fan_control_settings.timerEnd.seconds) > 0;
|
|
1645
1442
|
RESTTypeData.fan_timer_speed =
|
|
1646
1443
|
value.value.fan_control_settings.timerSpeed.includes('FAN_SPEED_SETTING_STAGE') === true &&
|
|
1647
1444
|
isNaN(value.value.fan_control_settings.timerSpeed.split('FAN_SPEED_SETTING_STAGE')[1]) === false
|
|
@@ -1662,31 +1459,23 @@ export default class NestAccfactory {
|
|
|
1662
1459
|
RESTTypeData.has_air_filter = value.value.hvac_equipment_capabilities.hasAirFilter === true;
|
|
1663
1460
|
RESTTypeData.filter_replacement_needed = value.value.filter_reminder.filterReplacementNeeded.value === true;
|
|
1664
1461
|
|
|
1462
|
+
// Hotwater details
|
|
1463
|
+
RESTTypeData.has_hot_water_control = value.value?.hvac_equipment_capabilities?.hasHotWaterControl === true;
|
|
1464
|
+
RESTTypeData.hot_water_active = value.value?.hot_water?.boilerActive === true;
|
|
1465
|
+
RESTTypeData.hot_water_boost_active =
|
|
1466
|
+
isNaN(value.value?.hot_water_settings?.boostTimerEnd?.seconds) === false &&
|
|
1467
|
+
Number(value.value.hot_water_settings.boostTimerEnd.seconds) > 0;
|
|
1468
|
+
|
|
1665
1469
|
// Process any temperature sensors associated with this thermostat
|
|
1666
1470
|
RESTTypeData.active_rcs_sensor =
|
|
1667
|
-
value.value
|
|
1471
|
+
value.value?.remote_comfort_sensing_settings?.activeRcsSelection?.activeRcsSensor !== undefined
|
|
1668
1472
|
? value.value.remote_comfort_sensing_settings.activeRcsSelection.activeRcsSensor.resourceId
|
|
1669
1473
|
: '';
|
|
1670
|
-
RESTTypeData.linked_rcs_sensors = [];
|
|
1671
|
-
if (value.value?.remote_comfort_sensing_settings?.associatedRcsSensors
|
|
1672
|
-
value.value.remote_comfort_sensing_settings.associatedRcsSensors.forEach((sensor) => {
|
|
1673
|
-
if (this.#rawData?.[sensor
|
|
1674
|
-
this.#rawData[sensor.deviceId.resourceId].value.associated_thermostat = object_key; // Sensor is linked to this thermostat
|
|
1675
|
-
|
|
1676
|
-
// Get sensor online/offline status
|
|
1677
|
-
// 'liveness' property doesn't appear in Protobuf data for temp sensors, so we'll add that object here
|
|
1678
|
-
this.#rawData[sensor.deviceId.resourceId].value.liveness = {};
|
|
1679
|
-
this.#rawData[sensor.deviceId.resourceId].value.liveness.status = 'LIVENESS_DEVICE_STATUS_UNSPECIFIED';
|
|
1680
|
-
if (value.value?.remote_comfort_sensing_state?.rcsSensorStatuses !== undefined) {
|
|
1681
|
-
Object.values(value.value.remote_comfort_sensing_state.rcsSensorStatuses).forEach((sensorStatus) => {
|
|
1682
|
-
if (
|
|
1683
|
-
sensorStatus?.sensorId?.resourceId === sensor.deviceId.resourceId &&
|
|
1684
|
-
sensorStatus?.dataRecency?.includes('OK') === true
|
|
1685
|
-
) {
|
|
1686
|
-
this.#rawData[sensor.deviceId.resourceId].value.liveness.status = 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
1687
|
-
}
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1474
|
+
RESTTypeData.linked_rcs_sensors = [];
|
|
1475
|
+
if (Array.isArray(value.value?.remote_comfort_sensing_settings?.associatedRcsSensors) === true) {
|
|
1476
|
+
value.value.remote_comfort_sensing_settings.associatedRcsSensors.forEach((sensor) => {
|
|
1477
|
+
if (typeof this.#rawData?.[sensor?.deviceId?.resourceId]?.value === 'object') {
|
|
1478
|
+
this.#rawData[sensor.deviceId.resourceId].value.associated_thermostat = object_key; // Sensor is linked to this thermostat
|
|
1690
1479
|
}
|
|
1691
1480
|
|
|
1692
1481
|
RESTTypeData.linked_rcs_sensors.push(sensor.deviceId.resourceId);
|
|
@@ -1694,7 +1483,7 @@ export default class NestAccfactory {
|
|
|
1694
1483
|
}
|
|
1695
1484
|
|
|
1696
1485
|
RESTTypeData.schedule_mode =
|
|
1697
|
-
value.value?.target_temperature_settings?.targetTemperature?.setpointType
|
|
1486
|
+
typeof value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'string' &&
|
|
1698
1487
|
value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase() !== 'off'
|
|
1699
1488
|
? value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase()
|
|
1700
1489
|
: '';
|
|
@@ -1705,7 +1494,7 @@ export default class NestAccfactory {
|
|
|
1705
1494
|
'SET_POINT_SCHEDULE_TYPE_' + RESTTypeData.schedule_mode.toUpperCase()
|
|
1706
1495
|
) {
|
|
1707
1496
|
Object.values(value.value[RESTTypeData.schedule_mode + '_schedule_settings'].setpoints).forEach((schedule) => {
|
|
1708
|
-
// Create
|
|
1497
|
+
// Create Nest API schedule entries
|
|
1709
1498
|
const DAYSOFWEEK = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'];
|
|
1710
1499
|
let dayofWeekIndex = DAYSOFWEEK.indexOf(schedule.dayOfWeek.split('DAY_OF_WEEK_')[1]);
|
|
1711
1500
|
|
|
@@ -1716,7 +1505,7 @@ export default class NestAccfactory {
|
|
|
1716
1505
|
RESTTypeData.schedules[dayofWeekIndex][Object.entries(RESTTypeData.schedules[dayofWeekIndex]).length] = {
|
|
1717
1506
|
'temp-min': adjustTemperature(schedule.heatingTarget.value, 'C', 'C', true),
|
|
1718
1507
|
'temp-max': adjustTemperature(schedule.coolingTarget.value, 'C', 'C', true),
|
|
1719
|
-
time: isNaN(schedule
|
|
1508
|
+
time: isNaN(schedule?.secondsInDay) === false ? Number(schedule.secondsInDay) : 0,
|
|
1720
1509
|
type: RESTTypeData.schedule_mode.toUpperCase(),
|
|
1721
1510
|
entry_type: 'setpoint',
|
|
1722
1511
|
};
|
|
@@ -1726,26 +1515,22 @@ export default class NestAccfactory {
|
|
|
1726
1515
|
tempDevice = process_thermostat_data(object_key, RESTTypeData);
|
|
1727
1516
|
}
|
|
1728
1517
|
|
|
1729
|
-
if (
|
|
1730
|
-
value?.source === NestAccfactory.DataSource.REST &&
|
|
1731
|
-
this.config.options?.restAPI === true &&
|
|
1732
|
-
value.value?.where_id !== undefined
|
|
1733
|
-
) {
|
|
1518
|
+
if (value?.source === DATASOURCE.NestAPI && this.config.options?.useNestAPI === true && value.value?.where_id !== undefined) {
|
|
1734
1519
|
let RESTTypeData = {};
|
|
1735
1520
|
RESTTypeData.serialNumber = value.value.serial_number;
|
|
1736
1521
|
RESTTypeData.softwareVersion = value.value.current_version;
|
|
1737
|
-
RESTTypeData.model = 'Thermostat';
|
|
1522
|
+
RESTTypeData.model = 'Thermostat (unknown)';
|
|
1738
1523
|
if (value.value.serial_number.substring(0, 2) === '15') {
|
|
1739
1524
|
RESTTypeData.model = 'Thermostat E (1st gen)'; // Nest Thermostat E
|
|
1740
1525
|
}
|
|
1741
|
-
if (value.value.serial_number.substring(0, 2) === '09') {
|
|
1742
|
-
RESTTypeData.model = 'Thermostat (3rd gen)'; // Nest Thermostat 3rd
|
|
1526
|
+
if (value.value.serial_number.substring(0, 2) === '09' || value.value.serial_number.substring(0, 2) === '10') {
|
|
1527
|
+
RESTTypeData.model = 'Learning Thermostat (3rd gen)'; // Nest Thermostat 3rd gen
|
|
1743
1528
|
}
|
|
1744
1529
|
if (value.value.serial_number.substring(0, 2) === '02') {
|
|
1745
|
-
RESTTypeData.model = 'Thermostat (2nd gen)'; // Nest Thermostat 2nd
|
|
1530
|
+
RESTTypeData.model = 'Learning Thermostat (2nd gen)'; // Nest Thermostat 2nd gen
|
|
1746
1531
|
}
|
|
1747
1532
|
if (value.value.serial_number.substring(0, 2) === '01') {
|
|
1748
|
-
RESTTypeData.model = 'Thermostat (1st gen)'; // Nest Thermostat 1st
|
|
1533
|
+
RESTTypeData.model = 'Learning Thermostat (1st gen)'; // Nest Thermostat 1st gen
|
|
1749
1534
|
}
|
|
1750
1535
|
RESTTypeData.current_humidity = value.value.current_humidity;
|
|
1751
1536
|
RESTTypeData.temperature_scale = value.value.temperature_scale;
|
|
@@ -1872,7 +1657,7 @@ export default class NestAccfactory {
|
|
|
1872
1657
|
}
|
|
1873
1658
|
|
|
1874
1659
|
// Update fan status, on or off
|
|
1875
|
-
RESTTypeData.fan_state = value.value.fan_timer_timeout
|
|
1660
|
+
RESTTypeData.fan_state = isNaN(value.value.fan_timer_timeout) === false && Number(value.value.fan_timer_timeout) > 0;
|
|
1876
1661
|
RESTTypeData.fan_timer_speed =
|
|
1877
1662
|
value.value.fan_timer_speed.includes('stage') === true && isNaN(value.value.fan_timer_speed.split('stage')[1]) === false
|
|
1878
1663
|
? Number(value.value.fan_timer_speed.split('stage')[1])
|
|
@@ -1891,6 +1676,12 @@ export default class NestAccfactory {
|
|
|
1891
1676
|
RESTTypeData.has_air_filter = value.value.has_air_filter === true;
|
|
1892
1677
|
RESTTypeData.filter_replacement_needed = value.value.filter_replacement_needed === true;
|
|
1893
1678
|
|
|
1679
|
+
// Hotwater details
|
|
1680
|
+
RESTTypeData.has_hot_water_control = value.value.has_hot_water_control === true;
|
|
1681
|
+
RESTTypeData.hot_water_active = value.value?.hot_water_active === true;
|
|
1682
|
+
RESTTypeData.hot_water_boost_active =
|
|
1683
|
+
isNaN(value.value?.hot_water_boost_time_to_end) === false && Number(value.value.hot_water_boost_time_to_end) > 0;
|
|
1684
|
+
|
|
1894
1685
|
// Process any temperature sensors associated with this thermostat
|
|
1895
1686
|
RESTTypeData.active_rcs_sensor = '';
|
|
1896
1687
|
RESTTypeData.linked_rcs_sensors = [];
|
|
@@ -1936,30 +1727,31 @@ export default class NestAccfactory {
|
|
|
1936
1727
|
}
|
|
1937
1728
|
// eslint-disable-next-line no-unused-vars
|
|
1938
1729
|
} catch (error) {
|
|
1939
|
-
this?.log?.debug
|
|
1730
|
+
this?.log?.debug?.('Error processing data for thermostat(s)');
|
|
1940
1731
|
}
|
|
1941
1732
|
|
|
1942
1733
|
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
|
|
1734
|
+
let deviceOptions = this.config?.devices?.find(
|
|
1735
|
+
(device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
|
|
1736
|
+
);
|
|
1943
1737
|
// Insert any extra options we've read in from configuration file for this device
|
|
1944
|
-
tempDevice.eveHistory =
|
|
1945
|
-
|
|
1946
|
-
tempDevice.humiditySensor = this.config?.devices?.[tempDevice.serialNumber]?.humiditySensor === true;
|
|
1738
|
+
tempDevice.eveHistory = this.config.options.eveHistory === true || deviceOptions?.eveHistory === true;
|
|
1739
|
+
tempDevice.humiditySensor = deviceOptions?.humiditySensor === true;
|
|
1947
1740
|
tempDevice.externalCool =
|
|
1948
|
-
typeof
|
|
1949
|
-
? this.config.devices[tempDevice.serialNumber].externalCool
|
|
1950
|
-
: undefined; // Config option for external cooling source
|
|
1741
|
+
typeof deviceOptions?.externalCool === 'string' && deviceOptions.externalCool !== '' ? deviceOptions.externalCool : undefined; // Config option for external cooling source
|
|
1951
1742
|
tempDevice.externalHeat =
|
|
1952
|
-
typeof
|
|
1953
|
-
? this.config.devices[tempDevice.serialNumber].externalHeat
|
|
1954
|
-
: undefined; // Config option for external heating source
|
|
1743
|
+
typeof deviceOptions?.externalHeat === 'string' && deviceOptions.externalHeat !== '' ? deviceOptions.externalHeat : undefined; // Config option for external heating source
|
|
1955
1744
|
tempDevice.externalFan =
|
|
1956
|
-
typeof
|
|
1957
|
-
? this.config.devices[tempDevice.serialNumber].externalFan
|
|
1958
|
-
: undefined; // Config option for external fan source
|
|
1745
|
+
typeof deviceOptions?.externalFan === 'string' && deviceOptions.externalFan !== '' ? deviceOptions.externalFan : undefined; // Config option for external fan source
|
|
1959
1746
|
tempDevice.externalDehumidifier =
|
|
1960
|
-
typeof
|
|
1961
|
-
?
|
|
1747
|
+
typeof deviceOptions?.externalDehumidifier === 'string' && deviceOptions.externalDehumidifier !== ''
|
|
1748
|
+
? deviceOptions.externalDehumidifier
|
|
1962
1749
|
: undefined; // Config option for external dehumidifier source
|
|
1750
|
+
tempDevice.hotWaterBoostTime = parseDurationToSeconds(deviceOptions?.hotWaterBoostTime, {
|
|
1751
|
+
defaultValue: 30 * 60, // 30mins
|
|
1752
|
+
min: 60, // 1min
|
|
1753
|
+
max: 7200, // 2hrs
|
|
1754
|
+
});
|
|
1963
1755
|
devices[tempDevice.serialNumber] = tempDevice; // Store processed device
|
|
1964
1756
|
}
|
|
1965
1757
|
});
|
|
@@ -1971,9 +1763,10 @@ export default class NestAccfactory {
|
|
|
1971
1763
|
let processed = {};
|
|
1972
1764
|
try {
|
|
1973
1765
|
// Fix up data we need to
|
|
1974
|
-
data =
|
|
1975
|
-
data.device_type = NestAccfactory.DeviceType.TEMPSENSOR; // Nest Temperature sensor
|
|
1766
|
+
data.device_type = DeviceType.TEMPSENSOR; // Nest Temperature sensor
|
|
1976
1767
|
data.model = 'Temperature Sensor';
|
|
1768
|
+
data.softwareVersion = '1.0.0';
|
|
1769
|
+
data = process_common_data(object_key, data);
|
|
1977
1770
|
data.current_temperature = adjustTemperature(data.current_temperature, 'C', 'C', true);
|
|
1978
1771
|
processed = data;
|
|
1979
1772
|
// eslint-disable-next-line no-unused-vars
|
|
@@ -1994,8 +1787,8 @@ export default class NestAccfactory {
|
|
|
1994
1787
|
let tempDevice = {};
|
|
1995
1788
|
try {
|
|
1996
1789
|
if (
|
|
1997
|
-
value?.source ===
|
|
1998
|
-
this.config.options?.
|
|
1790
|
+
value?.source === DATASOURCE.ProtobufAPI &&
|
|
1791
|
+
this.config.options?.useGoogleAPI === true &&
|
|
1999
1792
|
value.value?.configuration_done?.deviceReady === true &&
|
|
2000
1793
|
typeof value?.value?.associated_thermostat === 'string' &&
|
|
2001
1794
|
value?.value?.associated_thermostat !== ''
|
|
@@ -2005,8 +1798,9 @@ export default class NestAccfactory {
|
|
|
2005
1798
|
// Guessing battery minimum voltage is 2v??
|
|
2006
1799
|
RESTTypeData.battery_level = scaleValue(Number(value.value.battery.assessedVoltage.value), 2.0, 3.0, 0, 100);
|
|
2007
1800
|
RESTTypeData.current_temperature = value.value.current_temperature.temperatureValue.temperature.value;
|
|
2008
|
-
|
|
2009
|
-
|
|
1801
|
+
RESTTypeData.online =
|
|
1802
|
+
isNaN(value.value?.last_updated_beacon?.lastBeaconTime?.seconds) === false &&
|
|
1803
|
+
Math.floor(Date.now() / 1000) - Number(value.value.last_updated_beacon.lastBeaconTime.seconds) < 3600 * 4;
|
|
2010
1804
|
RESTTypeData.associated_thermostat = value.value.associated_thermostat;
|
|
2011
1805
|
RESTTypeData.description = typeof value.value?.label?.label === 'string' ? value.value.label.label : '';
|
|
2012
1806
|
RESTTypeData.location = get_location_name(
|
|
@@ -2019,8 +1813,8 @@ export default class NestAccfactory {
|
|
|
2019
1813
|
tempDevice = process_kryptonite_data(object_key, RESTTypeData);
|
|
2020
1814
|
}
|
|
2021
1815
|
if (
|
|
2022
|
-
value?.source ===
|
|
2023
|
-
this.config.options?.
|
|
1816
|
+
value?.source === DATASOURCE.NestAPI &&
|
|
1817
|
+
this.config.options?.useNestAPI === true &&
|
|
2024
1818
|
value.value?.where_id !== undefined &&
|
|
2025
1819
|
value.value?.structure_id !== undefined &&
|
|
2026
1820
|
typeof value?.value?.associated_thermostat === 'string' &&
|
|
@@ -2030,7 +1824,7 @@ export default class NestAccfactory {
|
|
|
2030
1824
|
RESTTypeData.serialNumber = value.value.serial_number;
|
|
2031
1825
|
RESTTypeData.battery_level = scaleValue(Number(value.value.battery_level), 0, 100, 0, 100);
|
|
2032
1826
|
RESTTypeData.current_temperature = value.value.current_temperature;
|
|
2033
|
-
RESTTypeData.online = Math.floor(Date.now() / 1000) - value.value.last_updated_at < 3600 * 4
|
|
1827
|
+
RESTTypeData.online = Math.floor(Date.now() / 1000) - value.value.last_updated_at < 3600 * 4;
|
|
2034
1828
|
RESTTypeData.associated_thermostat = value.value.associated_thermostat;
|
|
2035
1829
|
RESTTypeData.description = value.value.description;
|
|
2036
1830
|
RESTTypeData.location = get_location_name(value.value.structure_id, value.value.where_id);
|
|
@@ -2041,36 +1835,88 @@ export default class NestAccfactory {
|
|
|
2041
1835
|
}
|
|
2042
1836
|
// eslint-disable-next-line no-unused-vars
|
|
2043
1837
|
} catch (error) {
|
|
2044
|
-
this?.log?.debug
|
|
1838
|
+
this?.log?.debug?.('Error processing data for temperature sensor(s)');
|
|
2045
1839
|
}
|
|
2046
1840
|
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
|
|
1841
|
+
let deviceOptions = this.config?.devices?.find(
|
|
1842
|
+
(device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
|
|
1843
|
+
);
|
|
2047
1844
|
// Insert any extra options we've read in from configuration file for this device
|
|
2048
|
-
tempDevice.eveHistory =
|
|
2049
|
-
this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serialNumber]?.eveHistory === true;
|
|
1845
|
+
tempDevice.eveHistory = this.config.options.eveHistory === true || deviceOptions?.eveHistory === true;
|
|
2050
1846
|
devices[tempDevice.serialNumber] = tempDevice; // Store processed device
|
|
2051
1847
|
}
|
|
2052
1848
|
});
|
|
2053
1849
|
|
|
2054
|
-
// Process data for any
|
|
2055
|
-
const
|
|
1850
|
+
// Process data for any heatlink devices we have in the raw data
|
|
1851
|
+
const process_heatlink_data = (object_key, data) => {
|
|
2056
1852
|
let processed = {};
|
|
2057
1853
|
try {
|
|
2058
1854
|
// Fix up data we need to
|
|
1855
|
+
data.device_type = DeviceType.HEATLINK;
|
|
2059
1856
|
data = process_common_data(object_key, data);
|
|
2060
|
-
data.
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
1857
|
+
data.current_temperature = adjustTemperature(data.current_temperature, 'C', 'C', true);
|
|
1858
|
+
processed = data;
|
|
1859
|
+
// eslint-disable-next-line no-unused-vars
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
// Empty
|
|
1862
|
+
}
|
|
1863
|
+
return processed;
|
|
1864
|
+
};
|
|
1865
|
+
|
|
1866
|
+
Object.entries(this.#rawData)
|
|
1867
|
+
.filter(
|
|
1868
|
+
([key, value]) =>
|
|
1869
|
+
key.startsWith('DEVICE_') === true &&
|
|
1870
|
+
value.value?.device_info?.typeName === 'nest.resource.NestAgateHeatlinkResource' &&
|
|
1871
|
+
(deviceUUID === '' || deviceUUID === key),
|
|
1872
|
+
)
|
|
1873
|
+
.forEach(([object_key, value]) => {
|
|
1874
|
+
let tempDevice = {};
|
|
1875
|
+
try {
|
|
1876
|
+
if (
|
|
1877
|
+
value?.source === DATASOURCE.ProtobufAPI &&
|
|
1878
|
+
this.config.options?.useGoogleAPI === true &&
|
|
1879
|
+
value.value?.configuration_done?.deviceReady === true
|
|
1880
|
+
) {
|
|
1881
|
+
let RESTTypeData = {};
|
|
1882
|
+
RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
|
|
1883
|
+
RESTTypeData.softwareVersion = value.value.device_identity.softwareVersion;
|
|
1884
|
+
RESTTypeData.model = 'Heatlink (unknown)';
|
|
1885
|
+
if (value.value.device_info.typeName === 'nest.resource.NestAgateHeatlinkResource') {
|
|
1886
|
+
RESTTypeData.model = 'Heatlink';
|
|
1887
|
+
}
|
|
1888
|
+
RESTTypeData.battery_level = 100; // Not sure what it is
|
|
1889
|
+
RESTTypeData.current_temperature = value.value.temperature.temperatureValue.temperature.value;
|
|
1890
|
+
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
1891
|
+
RESTTypeData.description = typeof value.value?.label?.label === 'string' ? value.value.label.label : '';
|
|
1892
|
+
RESTTypeData.location = get_location_name(
|
|
1893
|
+
value.value?.device_info?.pairerId?.resourceId,
|
|
1894
|
+
value.value?.device_located_settings?.whereAnnotationRid?.resourceId,
|
|
1895
|
+
);
|
|
1896
|
+
RESTTypeData.active_sensor = true; // This should be active always?
|
|
1897
|
+
tempDevice = process_heatlink_data(object_key, RESTTypeData);
|
|
1898
|
+
}
|
|
1899
|
+
// eslint-disable-next-line no-unused-vars
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
this?.log?.debug?.('Error processing data for heatlink(s)');
|
|
2070
1902
|
}
|
|
2071
|
-
if (
|
|
2072
|
-
|
|
1903
|
+
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
|
|
1904
|
+
let deviceOptions = this.config?.devices?.find(
|
|
1905
|
+
(device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
|
|
1906
|
+
);
|
|
1907
|
+
// Insert any extra options we've read in from configuration file for this device
|
|
1908
|
+
tempDevice.eveHistory = this.config.options.eveHistory === true || deviceOptions?.eveHistory === true;
|
|
1909
|
+
devices[tempDevice.serialNumber] = tempDevice; // Store processed device
|
|
2073
1910
|
}
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// Process data for any smoke detectors we have in the raw data
|
|
1914
|
+
const process_protect_data = (object_key, data) => {
|
|
1915
|
+
let processed = {};
|
|
1916
|
+
try {
|
|
1917
|
+
// Fix up data we need to
|
|
1918
|
+
data.device_type = DeviceType.SMOKESENSOR; // Nest Protect
|
|
1919
|
+
data = process_common_data(object_key, data);
|
|
2074
1920
|
processed = data;
|
|
2075
1921
|
// eslint-disable-next-line no-unused-vars
|
|
2076
1922
|
} catch (error) {
|
|
@@ -2079,27 +1925,44 @@ export default class NestAccfactory {
|
|
|
2079
1925
|
return processed;
|
|
2080
1926
|
};
|
|
2081
1927
|
|
|
1928
|
+
const PROTOBUF_PROTECT_RESOURCES = [
|
|
1929
|
+
'nest.resource.NestProtect1LinePoweredResource',
|
|
1930
|
+
'nest.resource.NestProtect1BatteryPoweredResource',
|
|
1931
|
+
'nest.resource.NestProtect2LinePoweredResource',
|
|
1932
|
+
'nest.resource.NestProtect2BatteryPoweredResource',
|
|
1933
|
+
'NestProtect2Resource',
|
|
1934
|
+
];
|
|
2082
1935
|
Object.entries(this.#rawData)
|
|
2083
1936
|
.filter(
|
|
2084
1937
|
([key, value]) =>
|
|
2085
1938
|
(key.startsWith('topaz.') === true ||
|
|
2086
|
-
(key.startsWith('DEVICE_') === true && value.value?.device_info?.
|
|
1939
|
+
(key.startsWith('DEVICE_') === true && PROTOBUF_PROTECT_RESOURCES.includes(value.value?.device_info?.typeName) === true)) &&
|
|
2087
1940
|
(deviceUUID === '' || deviceUUID === key),
|
|
2088
1941
|
)
|
|
2089
1942
|
.forEach(([object_key, value]) => {
|
|
2090
1943
|
let tempDevice = {};
|
|
2091
1944
|
try {
|
|
2092
1945
|
if (
|
|
2093
|
-
value?.source ===
|
|
2094
|
-
this.config.options?.
|
|
1946
|
+
value?.source === DATASOURCE.ProtobufAPI &&
|
|
1947
|
+
this.config.options?.useGoogleAPI === true &&
|
|
2095
1948
|
value.value?.configuration_done?.deviceReady === true
|
|
2096
1949
|
) {
|
|
2097
1950
|
let RESTTypeData = {};
|
|
2098
1951
|
RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
|
|
2099
|
-
RESTTypeData.softwareVersion =
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
1952
|
+
RESTTypeData.softwareVersion = value.value.device_identity.softwareVersion;
|
|
1953
|
+
RESTTypeData.model = 'Protect (unknown)';
|
|
1954
|
+
if (value.value.device_info.typeName === 'nest.resource.NestProtect1LinePoweredResource') {
|
|
1955
|
+
RESTTypeData.model = 'Protect (1st gen, wired)';
|
|
1956
|
+
}
|
|
1957
|
+
if (value.value.device_info.typeName === 'nest.resource.NestProtect1BatteryPoweredResource') {
|
|
1958
|
+
RESTTypeData.model = 'Protect (1st gen, battery)';
|
|
1959
|
+
}
|
|
1960
|
+
if (value.value.device_info.typeName === 'nest.resource.NestProtect2LinePoweredResource') {
|
|
1961
|
+
RESTTypeData.model = 'Protect (2nd gen, wired)';
|
|
1962
|
+
}
|
|
1963
|
+
if (value.value.device_info.typeName === 'nest.resource.NestProtect2BatteryPoweredResource') {
|
|
1964
|
+
RESTTypeData.model = 'Protect (2nd gen, battery)';
|
|
1965
|
+
}
|
|
2103
1966
|
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
2104
1967
|
RESTTypeData.line_power_present = value.value?.wall_power?.status === 'POWER_SOURCE_STATUS_ACTIVE';
|
|
2105
1968
|
RESTTypeData.wired_or_battery = typeof value.value?.wall_power?.status === 'string' ? 0 : 1;
|
|
@@ -2114,7 +1977,7 @@ export default class NestAccfactory {
|
|
|
2114
1977
|
: 1;
|
|
2115
1978
|
RESTTypeData.smoke_status = value.value?.safety_alarm_smoke?.alarmState === 'ALARM_STATE_ALARM';
|
|
2116
1979
|
RESTTypeData.co_status = value.value?.safety_alarm_co?.alarmState === 'ALARM_STATE_ALARM';
|
|
2117
|
-
RESTTypeData.heat_status = false; //
|
|
1980
|
+
RESTTypeData.heat_status = false; // TODO <- need to find in protobuf
|
|
2118
1981
|
RESTTypeData.hushed_state =
|
|
2119
1982
|
value.value?.safety_alarm_smoke?.silenceState === 'SILENCE_STATE_SILENCED' ||
|
|
2120
1983
|
value.value?.safety_alarm_co?.silenceState === 'SILENCE_STATE_SILENCED';
|
|
@@ -2128,12 +1991,12 @@ export default class NestAccfactory {
|
|
|
2128
1991
|
? value.value.safety_summary?.warningDevices?.failures.includes('FAILURE_TYPE_TEMP') === false
|
|
2129
1992
|
: true;
|
|
2130
1993
|
RESTTypeData.latest_alarm_test =
|
|
2131
|
-
isNaN(value.value
|
|
1994
|
+
isNaN(value.value?.self_test?.lastMstEnd?.seconds) === false ? Number(value.value.self_test.lastMstEnd.seconds) : 0;
|
|
2132
1995
|
RESTTypeData.self_test_in_progress =
|
|
2133
1996
|
value.value?.legacy_structure_self_test?.mstInProgress === true ||
|
|
2134
1997
|
value.value?.legacy_structure_self_test?.astInProgress === true;
|
|
2135
1998
|
RESTTypeData.replacement_date =
|
|
2136
|
-
isNaN(value.value
|
|
1999
|
+
isNaN(value.value?.legacy_protect_device_settings?.replaceByDate?.seconds) === false
|
|
2137
2000
|
? Number(value.value.legacy_protect_device_settings.replaceByDate.seconds)
|
|
2138
2001
|
: 0;
|
|
2139
2002
|
RESTTypeData.topaz_hush_key =
|
|
@@ -2149,14 +2012,27 @@ export default class NestAccfactory {
|
|
|
2149
2012
|
tempDevice = process_protect_data(object_key, RESTTypeData);
|
|
2150
2013
|
}
|
|
2151
2014
|
if (
|
|
2152
|
-
value?.source ===
|
|
2153
|
-
this.config.options?.
|
|
2015
|
+
value?.source === DATASOURCE.NestAPI &&
|
|
2016
|
+
this.config.options?.useNestAPI === true &&
|
|
2154
2017
|
value.value?.where_id !== undefined &&
|
|
2155
2018
|
value.value?.structure_id !== undefined
|
|
2156
2019
|
) {
|
|
2157
2020
|
let RESTTypeData = {};
|
|
2158
2021
|
RESTTypeData.serialNumber = value.value.serial_number;
|
|
2159
2022
|
RESTTypeData.softwareVersion = value.value.software_version;
|
|
2023
|
+
RESTTypeData.model = 'Protect (unknown)';
|
|
2024
|
+
if (RESTTypeData.serialNumber.substring(0, 2) === '06') {
|
|
2025
|
+
RESTTypeData.model = 'Protect (2nd gen)'; // Nest Protect 2nd gen
|
|
2026
|
+
}
|
|
2027
|
+
if (RESTTypeData.serialNumber.substring(0, 2) === '05') {
|
|
2028
|
+
RESTTypeData.model = 'Protect (1st gen)'; // Nest Protect 1st gen
|
|
2029
|
+
}
|
|
2030
|
+
RESTTypeData.model =
|
|
2031
|
+
value.value.wired_or_battery === 1
|
|
2032
|
+
? RESTTypeData.model.replace(/\bgen\)/, 'gen, battery)')
|
|
2033
|
+
: value.value.wired_or_battery === 0
|
|
2034
|
+
? RESTTypeData.model.replace(/\bgen\)/, 'gen, wired)')
|
|
2035
|
+
: RESTTypeData.model;
|
|
2160
2036
|
RESTTypeData.online =
|
|
2161
2037
|
typeof value?.value?.thread_mac_address === 'string'
|
|
2162
2038
|
? this.#rawData?.['widget_track.' + value?.value?.thread_mac_address.toUpperCase()]?.value?.online === true
|
|
@@ -2187,13 +2063,15 @@ export default class NestAccfactory {
|
|
|
2187
2063
|
}
|
|
2188
2064
|
// eslint-disable-next-line no-unused-vars
|
|
2189
2065
|
} catch (error) {
|
|
2190
|
-
this?.log?.debug
|
|
2066
|
+
this?.log?.debug?.('Error processing data for smoke sensor(s)');
|
|
2191
2067
|
}
|
|
2192
2068
|
|
|
2193
2069
|
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
|
|
2070
|
+
let deviceOptions = this.config?.devices?.find(
|
|
2071
|
+
(device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
|
|
2072
|
+
);
|
|
2194
2073
|
// Insert any extra options we've read in from configuration file for this device
|
|
2195
|
-
tempDevice.eveHistory =
|
|
2196
|
-
this.config.options.eveHistory === true || this.config?.devices?.[tempDevice.serialNumber]?.eveHistory === true;
|
|
2074
|
+
tempDevice.eveHistory = this.config.options.eveHistory === true || deviceOptions?.eveHistory === true;
|
|
2197
2075
|
devices[tempDevice.serialNumber] = tempDevice; // Store processed device
|
|
2198
2076
|
}
|
|
2199
2077
|
});
|
|
@@ -2203,14 +2081,14 @@ export default class NestAccfactory {
|
|
|
2203
2081
|
let processed = {};
|
|
2204
2082
|
try {
|
|
2205
2083
|
// Fix up data we need to
|
|
2206
|
-
data =
|
|
2207
|
-
data.device_type = NestAccfactory.DeviceType.CAMERA;
|
|
2084
|
+
data.device_type = DeviceType.CAMERA;
|
|
2208
2085
|
if (data.model.toUpperCase().includes('DOORBELL') === true) {
|
|
2209
|
-
data.device_type =
|
|
2086
|
+
data.device_type = DeviceType.DOORBELL;
|
|
2210
2087
|
}
|
|
2211
2088
|
if (data.model.toUpperCase().includes('FLOODLIGHT') === true) {
|
|
2212
|
-
data.device_type =
|
|
2089
|
+
data.device_type = DeviceType.FLOODLIGHT;
|
|
2213
2090
|
}
|
|
2091
|
+
data = process_common_data(object_key, data);
|
|
2214
2092
|
processed = data;
|
|
2215
2093
|
// eslint-disable-next-line no-unused-vars
|
|
2216
2094
|
} catch (error) {
|
|
@@ -2242,8 +2120,8 @@ export default class NestAccfactory {
|
|
|
2242
2120
|
let tempDevice = {};
|
|
2243
2121
|
try {
|
|
2244
2122
|
if (
|
|
2245
|
-
value?.source ===
|
|
2246
|
-
this.config.options?.
|
|
2123
|
+
value?.source === DATASOURCE.ProtobufAPI &&
|
|
2124
|
+
this.config.options?.useGoogleAPI === true &&
|
|
2247
2125
|
Array.isArray(value.value?.streaming_protocol?.supportedProtocols) === true &&
|
|
2248
2126
|
value.value.streaming_protocol.supportedProtocols.includes('PROTOCOL_WEBRTC') === true &&
|
|
2249
2127
|
(value.value?.configuration_done?.deviceReady === true ||
|
|
@@ -2251,15 +2129,8 @@ export default class NestAccfactory {
|
|
|
2251
2129
|
) {
|
|
2252
2130
|
let RESTTypeData = {};
|
|
2253
2131
|
RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
// ie: rquartz-user 1 OPENMASTER 507800056 test-keys stable-channel stable-channel
|
|
2257
|
-
// ie: nq-user 1.73 OPENMASTER 422270 release-keys stable-channel stable-channel
|
|
2258
|
-
RESTTypeData.softwareVersion =
|
|
2259
|
-
value.value.device_identity.softwareVersion.split(/\s+/)?.[3] !== undefined
|
|
2260
|
-
? value.value.device_identity.softwareVersion.split(/\s+/)?.[3]
|
|
2261
|
-
: value.value.device_identity.softwareVersion;
|
|
2262
|
-
RESTTypeData.model = 'Camera';
|
|
2132
|
+
RESTTypeData.softwareVersion = value.value.device_identity.softwareVersion;
|
|
2133
|
+
RESTTypeData.model = 'Camera (unknown)';
|
|
2263
2134
|
if (
|
|
2264
2135
|
value.value.device_info.typeName === 'google.resource.NeonQuartzResource' &&
|
|
2265
2136
|
value.value?.floodlight_settings === undefined &&
|
|
@@ -2268,13 +2139,13 @@ export default class NestAccfactory {
|
|
|
2268
2139
|
RESTTypeData.model = 'Cam (battery)';
|
|
2269
2140
|
}
|
|
2270
2141
|
if (value.value.device_info.typeName === 'google.resource.GreenQuartzResource') {
|
|
2271
|
-
RESTTypeData.model = 'Doorbell (battery)';
|
|
2142
|
+
RESTTypeData.model = 'Doorbell (2nd gen, battery)';
|
|
2272
2143
|
}
|
|
2273
2144
|
if (value.value.device_info.typeName === 'google.resource.SpencerResource') {
|
|
2274
2145
|
RESTTypeData.model = 'Cam (wired)';
|
|
2275
2146
|
}
|
|
2276
2147
|
if (value.value.device_info.typeName === 'google.resource.VenusResource') {
|
|
2277
|
-
RESTTypeData.model = 'Doorbell (
|
|
2148
|
+
RESTTypeData.model = 'Doorbell (2nd gen, wired)';
|
|
2278
2149
|
}
|
|
2279
2150
|
if (value.value.device_info.typeName === 'nest.resource.NestCamIndoorResource') {
|
|
2280
2151
|
RESTTypeData.model = 'Cam Indoor (1st gen)';
|
|
@@ -2286,14 +2157,14 @@ export default class NestAccfactory {
|
|
|
2286
2157
|
RESTTypeData.model = 'Cam Outdoor (1st gen)';
|
|
2287
2158
|
}
|
|
2288
2159
|
if (value.value.device_info.typeName === 'nest.resource.NestHelloResource') {
|
|
2289
|
-
RESTTypeData.model = 'Doorbell (
|
|
2160
|
+
RESTTypeData.model = 'Doorbell (1st gen, wired)';
|
|
2290
2161
|
}
|
|
2291
2162
|
if (
|
|
2292
2163
|
value.value.device_info.typeName === 'google.resource.NeonQuartzResource' &&
|
|
2293
2164
|
value.value?.floodlight_settings !== undefined &&
|
|
2294
2165
|
value.value?.floodlight_state !== undefined
|
|
2295
2166
|
) {
|
|
2296
|
-
RESTTypeData.model = 'Cam with Floodlight
|
|
2167
|
+
RESTTypeData.model = 'Cam with Floodlight';
|
|
2297
2168
|
}
|
|
2298
2169
|
|
|
2299
2170
|
RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
|
|
@@ -2308,6 +2179,7 @@ export default class NestAccfactory {
|
|
|
2308
2179
|
value.value?.doorbell_indoor_chime_settings?.chimeType === 'CHIME_TYPE_ELECTRONIC';
|
|
2309
2180
|
RESTTypeData.indoor_chime_enabled = value.value?.doorbell_indoor_chime_settings?.chimeEnabled === true;
|
|
2310
2181
|
RESTTypeData.streaming_enabled = value.value?.recording_toggle?.currentCameraState === 'CAMERA_ON';
|
|
2182
|
+
// Still need to find below in protobuf
|
|
2311
2183
|
//RESTTypeData.has_irled =
|
|
2312
2184
|
//RESTTypeData.irled_enabled =
|
|
2313
2185
|
//RESTTypeData.has_statusled =
|
|
@@ -2328,8 +2200,9 @@ export default class NestAccfactory {
|
|
|
2328
2200
|
}
|
|
2329
2201
|
RESTTypeData.alerts = typeof value.value?.alerts === 'object' ? value.value.alerts : [];
|
|
2330
2202
|
RESTTypeData.quiet_time_enabled =
|
|
2331
|
-
|
|
2332
|
-
|
|
2203
|
+
isNaN(value.value?.quiet_time_settings?.quietTimeEnds?.seconds) === false &&
|
|
2204
|
+
Number(value.value.quiet_time_settings.quietTimeEnds.seconds) !== 0 &&
|
|
2205
|
+
Math.floor(Date.now() / 1000) < Number(value.value.quiet_time_settings.quietTimeEnds.second);
|
|
2333
2206
|
RESTTypeData.camera_type = value.value.device_identity.vendorProductId;
|
|
2334
2207
|
RESTTypeData.streaming_protocols =
|
|
2335
2208
|
value.value?.streaming_protocol?.supportedProtocols !== undefined ? value.value.streaming_protocol.supportedProtocols : [];
|
|
@@ -2340,7 +2213,7 @@ export default class NestAccfactory {
|
|
|
2340
2213
|
RESTTypeData.has_light = value.value?.floodlight_settings !== undefined && value.value?.floodlight_state !== undefined;
|
|
2341
2214
|
RESTTypeData.light_enabled = value.value?.floodlight_state?.currentState === 'LIGHT_STATE_ON';
|
|
2342
2215
|
RESTTypeData.light_brightness =
|
|
2343
|
-
value.value?.floodlight_settings?.brightness
|
|
2216
|
+
isNaN(value.value?.floodlight_settings?.brightness) === false
|
|
2344
2217
|
? scaleValue(Number(value.value.floodlight_settings.brightness), 0, 10, 0, 100)
|
|
2345
2218
|
: 0;
|
|
2346
2219
|
|
|
@@ -2357,15 +2230,15 @@ export default class NestAccfactory {
|
|
|
2357
2230
|
}
|
|
2358
2231
|
|
|
2359
2232
|
if (
|
|
2360
|
-
value?.source ===
|
|
2361
|
-
this.config.options?.
|
|
2233
|
+
value?.source === DATASOURCE.NestAPI &&
|
|
2234
|
+
this.config.options?.useNestAPI === true &&
|
|
2362
2235
|
value.value?.where_id !== undefined &&
|
|
2363
2236
|
value.value?.structure_id !== undefined &&
|
|
2364
2237
|
value.value?.nexus_api_http_server_url !== undefined &&
|
|
2365
2238
|
(value.value?.properties?.['cc2migration.overview_state'] === 'NORMAL' ||
|
|
2366
2239
|
value.value?.properties?.['cc2migration.overview_state'] === 'REVERSE_MIGRATION_IN_PROGRESS')
|
|
2367
2240
|
) {
|
|
2368
|
-
// We'll only use the
|
|
2241
|
+
// We'll only use the Nest API data for Camera's which have NOT been migrated to Google Home
|
|
2369
2242
|
let RESTTypeData = {};
|
|
2370
2243
|
RESTTypeData.serialNumber = value.value.serial_number;
|
|
2371
2244
|
RESTTypeData.softwareVersion = value.value.software_version;
|
|
@@ -2385,7 +2258,7 @@ export default class NestAccfactory {
|
|
|
2385
2258
|
RESTTypeData.video_flipped = value.value?.properties['video.flipped'] === true;
|
|
2386
2259
|
RESTTypeData.statusled_brightness =
|
|
2387
2260
|
isNaN(value.value?.properties?.['statusled.brightness']) === false
|
|
2388
|
-
? Number(value.value
|
|
2261
|
+
? Number(value.value.properties['statusled.brightness'])
|
|
2389
2262
|
: 0;
|
|
2390
2263
|
RESTTypeData.has_microphone = value.value?.capabilities.includes('audio.microphone') === true;
|
|
2391
2264
|
RESTTypeData.has_speaker = value.value?.capabilities.includes('audio.speaker') === true;
|
|
@@ -2409,55 +2282,43 @@ export default class NestAccfactory {
|
|
|
2409
2282
|
}
|
|
2410
2283
|
// eslint-disable-next-line no-unused-vars
|
|
2411
2284
|
} catch (error) {
|
|
2412
|
-
this?.log?.debug
|
|
2285
|
+
this?.log?.debug?.('Error processing data for camera/doorbell(s)');
|
|
2413
2286
|
}
|
|
2414
2287
|
|
|
2415
2288
|
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
|
|
2289
|
+
let deviceOptions = this.config?.devices?.find(
|
|
2290
|
+
(device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
|
|
2291
|
+
);
|
|
2416
2292
|
// Insert any extra options we've read in from configuration file for this device
|
|
2417
|
-
tempDevice.eveHistory =
|
|
2418
|
-
|
|
2419
|
-
tempDevice.
|
|
2420
|
-
tempDevice.
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
tempDevice.motionCooldown =
|
|
2425
|
-
isNaN(this.config?.devices?.[tempDevice.serialNumber]?.motionCooldown) === false
|
|
2426
|
-
? Number(this.config.devices[tempDevice.serialNumber].motionCooldown)
|
|
2427
|
-
: 60;
|
|
2428
|
-
tempDevice.personCooldown =
|
|
2429
|
-
isNaN(this.config?.devices?.[tempDevice.serialNumber]?.personCooldown) === false
|
|
2430
|
-
? Number(this.config.devices[tempDevice.serialNumber].personCooldown)
|
|
2431
|
-
: 120;
|
|
2432
|
-
tempDevice.chimeSwitch = this.config?.devices?.[tempDevice.serialNumber]?.chimeSwitch === true; // Control 'indoor' chime by switch
|
|
2433
|
-
tempDevice.localAccess = this.config?.devices?.[tempDevice.serialNumber]?.localAccess === true; // Local network video streaming rather than from cloud from camera/doorbells
|
|
2293
|
+
tempDevice.eveHistory = this.config.options.eveHistory === true || deviceOptions?.eveHistory === true;
|
|
2294
|
+
tempDevice.hksv = this.config.options.hksv === true || deviceOptions?.hksv === true;
|
|
2295
|
+
tempDevice.doorbellCooldown = parseDurationToSeconds(deviceOptions?.doorbellCooldown, { defaultValue: 60, min: 0, max: 300 });
|
|
2296
|
+
tempDevice.motionCooldown = parseDurationToSeconds(deviceOptions?.motionCooldown, { defaultValue: 60, min: 0, max: 300 });
|
|
2297
|
+
tempDevice.personCooldown = parseDurationToSeconds(deviceOptions?.personCooldown, { defaultValue: 120, min: 0, max: 300 });
|
|
2298
|
+
tempDevice.chimeSwitch = deviceOptions?.chimeSwitch === true; // Control 'indoor' chime by switch
|
|
2299
|
+
tempDevice.localAccess = deviceOptions?.localAccess === true; // Local network video streaming rather than from cloud from camera/doorbells
|
|
2434
2300
|
// eslint-disable-next-line no-undef
|
|
2435
2301
|
tempDevice.ffmpeg = structuredClone(this.config.options.ffmpeg); // ffmpeg details, path, libraries. No ffmpeg = undefined
|
|
2436
|
-
if (
|
|
2302
|
+
if (deviceOptions?.ffmpegDebug !== undefined) {
|
|
2437
2303
|
// Device specific ffmpeg debugging
|
|
2438
|
-
tempDevice.ffmpeg.debug =
|
|
2304
|
+
tempDevice.ffmpeg.debug = deviceOptions?.ffmpegDebug === true;
|
|
2439
2305
|
}
|
|
2440
|
-
tempDevice.maxStreams =
|
|
2441
|
-
isNaN(this.config.options?.maxStreams) === false
|
|
2442
|
-
? Number(this.config.options.maxStreams)
|
|
2443
|
-
: this.deviceData.hksv === true
|
|
2444
|
-
? 1
|
|
2445
|
-
: 2;
|
|
2446
|
-
|
|
2306
|
+
tempDevice.maxStreams = this.config.options.hksv === true || deviceOptions?.hksv === true ? 1 : 2;
|
|
2447
2307
|
devices[tempDevice.serialNumber] = tempDevice; // Store processed device
|
|
2448
2308
|
}
|
|
2449
2309
|
});
|
|
2450
2310
|
|
|
2451
|
-
// Process data for any structure(s) for both
|
|
2311
|
+
// Process data for any structure(s) for both Nest and Protobuf API data
|
|
2452
2312
|
// We use this to created virtual weather station(s) for each structure that has location data
|
|
2453
2313
|
const process_structure_data = (object_key, data) => {
|
|
2454
2314
|
let processed = {};
|
|
2455
2315
|
try {
|
|
2456
2316
|
// Fix up data we need to
|
|
2457
|
-
data =
|
|
2458
|
-
data.device_type = NestAccfactory.DeviceType.WEATHER;
|
|
2317
|
+
data.device_type = DeviceType.WEATHER;
|
|
2459
2318
|
data.model = 'Weather';
|
|
2460
|
-
data.
|
|
2319
|
+
data.softwareVersion = '1.0.0';
|
|
2320
|
+
data = process_common_data(object_key, data);
|
|
2321
|
+
data.current_temperature = adjustTemperature(data.weather.current_temperature, 'C', 'C', true);
|
|
2461
2322
|
data.current_humidity = data.weather.current_humidity;
|
|
2462
2323
|
data.condition = data.weather.condition;
|
|
2463
2324
|
data.wind_direction = data.weather.wind_direction;
|
|
@@ -2466,12 +2327,6 @@ export default class NestAccfactory {
|
|
|
2466
2327
|
data.sunset = data.weather.sunset;
|
|
2467
2328
|
data.station = data.weather.station;
|
|
2468
2329
|
data.forecast = data.weather.forecast;
|
|
2469
|
-
data.elevation =
|
|
2470
|
-
isNaN(this.config?.devices?.[data?.serial_number]?.elevation) === false
|
|
2471
|
-
? Number(this.config.devices[data.serial_number].elevation)
|
|
2472
|
-
: isNaN(this.config?.options?.elevation) === false
|
|
2473
|
-
? Number(this.config.options.elevation)
|
|
2474
|
-
: 0;
|
|
2475
2330
|
processed = data;
|
|
2476
2331
|
// eslint-disable-next-line no-unused-vars
|
|
2477
2332
|
} catch (error) {
|
|
@@ -2491,8 +2346,8 @@ export default class NestAccfactory {
|
|
|
2491
2346
|
let tempDevice = {};
|
|
2492
2347
|
try {
|
|
2493
2348
|
if (
|
|
2494
|
-
value?.source ===
|
|
2495
|
-
this.config.options?.
|
|
2349
|
+
value?.source === DATASOURCE.ProtobufAPI &&
|
|
2350
|
+
this.config.options?.useGoogleAPI === true &&
|
|
2496
2351
|
value.value?.structure_location?.geoCoordinate?.latitude !== undefined &&
|
|
2497
2352
|
value.value?.structure_location?.geoCoordinate?.longitude !== undefined
|
|
2498
2353
|
) {
|
|
@@ -2510,14 +2365,14 @@ export default class NestAccfactory {
|
|
|
2510
2365
|
: value.value.structure_info.name;
|
|
2511
2366
|
RESTTypeData.weather = value.value.weather;
|
|
2512
2367
|
|
|
2513
|
-
// Use the
|
|
2368
|
+
// Use the Nest API structure ID from the Protobuf structure. This will ensure we generate the same serial number
|
|
2514
2369
|
// This should prevent two 'weather' objects being created
|
|
2515
2370
|
tempDevice = process_structure_data(object_key, RESTTypeData);
|
|
2516
2371
|
}
|
|
2517
2372
|
|
|
2518
2373
|
if (
|
|
2519
|
-
value?.source ===
|
|
2520
|
-
this.config.options?.
|
|
2374
|
+
value?.source === DATASOURCE.NestAPI &&
|
|
2375
|
+
this.config.options?.useNestAPI === true &&
|
|
2521
2376
|
value.value?.latitude !== undefined &&
|
|
2522
2377
|
value.value?.longitude !== undefined
|
|
2523
2378
|
) {
|
|
@@ -2536,13 +2391,19 @@ export default class NestAccfactory {
|
|
|
2536
2391
|
}
|
|
2537
2392
|
// eslint-disable-next-line no-unused-vars
|
|
2538
2393
|
} catch (error) {
|
|
2539
|
-
this?.log?.debug
|
|
2394
|
+
this?.log?.debug?.('Error processing data for weather');
|
|
2540
2395
|
}
|
|
2541
2396
|
|
|
2542
2397
|
if (Object.entries(tempDevice).length !== 0 && typeof devices[tempDevice.serialNumber] === 'undefined') {
|
|
2398
|
+
let deviceOptions = this.config?.devices?.find(
|
|
2399
|
+
(device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
|
|
2400
|
+
);
|
|
2543
2401
|
// Insert any extra options we've read in from configuration file for this device
|
|
2544
|
-
tempDevice.eveHistory =
|
|
2545
|
-
|
|
2402
|
+
tempDevice.eveHistory = this.config.options.eveHistory === true || deviceOptions?.eveHistory === true;
|
|
2403
|
+
tempDevice.elevation =
|
|
2404
|
+
isNaN(deviceOptions?.elevation) === false && Number(deviceOptions?.elevation) >= 0 && Number(deviceOptions?.elevation) <= 8848
|
|
2405
|
+
? Number(deviceOptions?.elevation)
|
|
2406
|
+
: this.config.options.elevation;
|
|
2546
2407
|
devices[tempDevice.serialNumber] = tempDevice; // Store processed device
|
|
2547
2408
|
}
|
|
2548
2409
|
});
|
|
@@ -2550,10 +2411,8 @@ export default class NestAccfactory {
|
|
|
2550
2411
|
return devices; // Return our processed data
|
|
2551
2412
|
}
|
|
2552
2413
|
|
|
2553
|
-
async #set(
|
|
2414
|
+
async #set(values) {
|
|
2554
2415
|
if (
|
|
2555
|
-
typeof uuid !== 'string' ||
|
|
2556
|
-
uuid === '' ||
|
|
2557
2416
|
typeof values !== 'object' ||
|
|
2558
2417
|
values?.uuid === undefined ||
|
|
2559
2418
|
typeof this.#rawData?.[values?.uuid] !== 'object' ||
|
|
@@ -2563,9 +2422,9 @@ export default class NestAccfactory {
|
|
|
2563
2422
|
}
|
|
2564
2423
|
|
|
2565
2424
|
let nest_google_uuid = values.uuid; // Nest/Google structure uuid for this get request
|
|
2566
|
-
let
|
|
2425
|
+
let uuid = this.#rawData[values.uuid].connection; // Connection uuid for this device
|
|
2567
2426
|
|
|
2568
|
-
if (this.#protobufRoot !== null && this.#rawData?.[nest_google_uuid]?.source ===
|
|
2427
|
+
if (this.#protobufRoot !== null && this.#rawData?.[nest_google_uuid]?.source === DATASOURCE.ProtobufAPI) {
|
|
2569
2428
|
let updatedTraits = [];
|
|
2570
2429
|
let protobufElement = {
|
|
2571
2430
|
traitRequest: {
|
|
@@ -2636,7 +2495,7 @@ export default class NestAccfactory {
|
|
|
2636
2495
|
protobufElement.state.value.enabled = { value: false };
|
|
2637
2496
|
}
|
|
2638
2497
|
|
|
2639
|
-
// Tag 'who is doing the temperature/mode change. We are :-)
|
|
2498
|
+
// Tag 'who' is doing the temperature/mode change. We are :-)
|
|
2640
2499
|
protobufElement.state.value.targetTemperature.currentActorInfo = {
|
|
2641
2500
|
method: 'HVAC_ACTOR_METHOD_IOS',
|
|
2642
2501
|
originator: this.#rawData[nest_google_uuid].value.target_temperature_settings.targetTemperature.currentActorInfo.originator,
|
|
@@ -2709,9 +2568,11 @@ export default class NestAccfactory {
|
|
|
2709
2568
|
protobufElement.state.value.timerEnd =
|
|
2710
2569
|
value === true
|
|
2711
2570
|
? {
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2571
|
+
seconds: Number(Math.floor(Date.now() / 1000) + Number(protobufElement.state.value.timerDuration.seconds)),
|
|
2572
|
+
nanos: Number(
|
|
2573
|
+
((Math.floor(Date.now() / 1000) + Number(protobufElement.state.value.timerDuration.seconds)) % 1000) * 1e6,
|
|
2574
|
+
),
|
|
2575
|
+
}
|
|
2715
2576
|
: { seconds: 0, nanos: 0 };
|
|
2716
2577
|
if (values?.fan_timer_speed !== undefined) {
|
|
2717
2578
|
// We have a value to set fan speed also, so handle here as combined setting
|
|
@@ -2748,7 +2609,10 @@ export default class NestAccfactory {
|
|
|
2748
2609
|
protobufElement.state.value = this.#rawData[nest_google_uuid].value.recording_toggle_settings;
|
|
2749
2610
|
protobufElement.state.value.targetCameraState = value === true ? 'CAMERA_ON' : 'CAMERA_OFF';
|
|
2750
2611
|
protobufElement.state.value.changeModeReason = 2;
|
|
2751
|
-
protobufElement.state.value.settingsUpdated = {
|
|
2612
|
+
protobufElement.state.value.settingsUpdated = {
|
|
2613
|
+
seconds: Math.floor(Date.now() / 1000),
|
|
2614
|
+
nanos: (Date.now() % 1000) * 1e6,
|
|
2615
|
+
};
|
|
2752
2616
|
}
|
|
2753
2617
|
|
|
2754
2618
|
if (key === 'audio_enabled' && typeof value === 'boolean') {
|
|
@@ -2781,7 +2645,7 @@ export default class NestAccfactory {
|
|
|
2781
2645
|
});
|
|
2782
2646
|
|
|
2783
2647
|
if (serviceUUID !== undefined) {
|
|
2784
|
-
let commandResponse = await this.#protobufCommand(
|
|
2648
|
+
let commandResponse = await this.#protobufCommand(uuid, 'ResourceApi', 'SendCommand', {
|
|
2785
2649
|
resourceRequest: {
|
|
2786
2650
|
resourceId: serviceUUID,
|
|
2787
2651
|
requestId: crypto.randomUUID(),
|
|
@@ -2800,7 +2664,7 @@ export default class NestAccfactory {
|
|
|
2800
2664
|
});
|
|
2801
2665
|
|
|
2802
2666
|
if (commandResponse.sendCommandResponse?.[0]?.traitOperations?.[0]?.progress !== 'COMPLETE') {
|
|
2803
|
-
this?.log?.debug
|
|
2667
|
+
this?.log?.debug?.('Protobuf API had error setting light status on uuid "%s"', nest_google_uuid);
|
|
2804
2668
|
}
|
|
2805
2669
|
}
|
|
2806
2670
|
}
|
|
@@ -2814,8 +2678,42 @@ export default class NestAccfactory {
|
|
|
2814
2678
|
protobufElement.state.value.brightness = scaleValue(Number(value), 0, 100, 0, 10); // Scale to required level
|
|
2815
2679
|
}
|
|
2816
2680
|
|
|
2681
|
+
if (
|
|
2682
|
+
key === 'active_sensor' &&
|
|
2683
|
+
typeof value === 'boolean' &&
|
|
2684
|
+
typeof this.#rawData?.[this.#rawData[nest_google_uuid]?.value?.associated_thermostat]?.value
|
|
2685
|
+
?.remote_comfort_sensing_settings === 'object'
|
|
2686
|
+
) {
|
|
2687
|
+
// Set active temperature sensor for associated thermostat
|
|
2688
|
+
protobufElement.traitRequest.resourceId = this.#rawData[nest_google_uuid].value.associated_thermostat;
|
|
2689
|
+
protobufElement.traitRequest.traitLabel = 'remote_comfort_sensing_settings';
|
|
2690
|
+
protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.RemoteComfortSensingSettingsTrait';
|
|
2691
|
+
protobufElement.state.value =
|
|
2692
|
+
this.#rawData[this.#rawData[nest_google_uuid].value.associated_thermostat].value.remote_comfort_sensing_settings;
|
|
2693
|
+
protobufElement.state.value.activeRcsSelection =
|
|
2694
|
+
value === true
|
|
2695
|
+
? { rcsSourceType: 'RCS_SOURCE_TYPE_SINGLE_SENSOR', activeRcsSensor: { resourceId: nest_google_uuid } }
|
|
2696
|
+
: { rcsSourceType: 'RCS_SOURCE_TYPE_BACKPLATE' };
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
if (key === 'hot_water_boost_active' && typeof value === 'object') {
|
|
2700
|
+
// Turn hotwater boost heating on/off
|
|
2701
|
+
protobufElement.traitRequest.traitLabel = 'hot_water_settings';
|
|
2702
|
+
protobufElement.state.type_url = 'type.nestlabs.com/nest.trait.hvac.HotWaterSettingsTrait';
|
|
2703
|
+
protobufElement.state.value = this.#rawData[nest_google_uuid].value.hot_water_settings;
|
|
2704
|
+
protobufElement.state.value.boostTimerEnd =
|
|
2705
|
+
value?.state === true
|
|
2706
|
+
? {
|
|
2707
|
+
seconds: Number(Math.floor(Date.now() / 1000) + Number(isNaN(value?.time) === false ? value?.time : 30 * 60)),
|
|
2708
|
+
nanos: Number(
|
|
2709
|
+
(Math.floor(Date.now() / 1000) + (Number(isNaN(value?.time) === false ? value?.time : 30 * 60) % 1000)) * 1e6,
|
|
2710
|
+
),
|
|
2711
|
+
}
|
|
2712
|
+
: { seconds: 0, nanos: 0 };
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2817
2715
|
if (protobufElement.traitRequest.traitLabel === '' || protobufElement.state.type_url === '') {
|
|
2818
|
-
this?.log?.debug
|
|
2716
|
+
this?.log?.debug?.('Unknown Protobuf set key "%s" for device uuid "%s"', key, nest_google_uuid);
|
|
2819
2717
|
}
|
|
2820
2718
|
|
|
2821
2719
|
if (protobufElement.traitRequest.traitLabel !== '' && protobufElement.state.type_url !== '') {
|
|
@@ -2826,90 +2724,94 @@ export default class NestAccfactory {
|
|
|
2826
2724
|
);
|
|
2827
2725
|
|
|
2828
2726
|
if (updatedTraits.length !== 0) {
|
|
2829
|
-
let commandResponse = await this.#protobufCommand(
|
|
2727
|
+
let commandResponse = await this.#protobufCommand(uuid, 'TraitBatchApi', 'BatchUpdateState', {
|
|
2830
2728
|
batchUpdateStateRequest: updatedTraits,
|
|
2831
2729
|
});
|
|
2832
2730
|
if (
|
|
2833
2731
|
commandResponse === undefined ||
|
|
2834
2732
|
commandResponse?.batchUpdateStateResponse?.[0]?.traitOperations?.[0]?.progress !== 'COMPLETE'
|
|
2835
2733
|
) {
|
|
2836
|
-
this?.log?.debug
|
|
2734
|
+
this?.log?.debug?.('Protobuf API had error updating device traits for uuid "%s"', nest_google_uuid);
|
|
2837
2735
|
}
|
|
2838
2736
|
}
|
|
2839
2737
|
}
|
|
2840
2738
|
|
|
2841
|
-
if (this.#rawData?.[nest_google_uuid]?.source ===
|
|
2739
|
+
if (this.#rawData?.[nest_google_uuid]?.source === DATASOURCE.NestAPI && nest_google_uuid.startsWith('quartz.') === true) {
|
|
2842
2740
|
// Set value on Nest Camera/Doorbell
|
|
2843
2741
|
await Promise.all(
|
|
2844
2742
|
Object.entries(values)
|
|
2845
2743
|
.filter(([key]) => key !== 'uuid')
|
|
2846
2744
|
.map(async ([key, value]) => {
|
|
2847
|
-
|
|
2848
|
-
indoor_chime_enabled: 'doorbell.indoor_chime.enabled',
|
|
2849
|
-
statusled_brightness: 'statusled.brightness',
|
|
2850
|
-
irled_enabled: 'irled.state',
|
|
2851
|
-
streaming_enabled: 'streaming.enabled',
|
|
2852
|
-
audio_enabled: 'audio.enabled',
|
|
2853
|
-
};
|
|
2854
|
-
|
|
2855
|
-
// Transform key to correct set camera properties key
|
|
2856
|
-
key = SETPROPERTIES[key] !== undefined ? SETPROPERTIES[key] : key;
|
|
2857
|
-
|
|
2858
|
-
await fetchWrapper(
|
|
2859
|
-
'post',
|
|
2860
|
-
'https://webapi.' + this.#connections[connectionUuid].cameraAPIHost + '/api/dropcams.set_properties',
|
|
2745
|
+
let mappedKey =
|
|
2861
2746
|
{
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2747
|
+
indoor_chime_enabled: 'doorbell.indoor_chime.enabled',
|
|
2748
|
+
statusled_brightness: 'statusled.brightness',
|
|
2749
|
+
irled_enabled: 'irled.state',
|
|
2750
|
+
streaming_enabled: 'streaming.enabled',
|
|
2751
|
+
audio_enabled: 'audio.enabled',
|
|
2752
|
+
}[key] ?? key;
|
|
2753
|
+
|
|
2754
|
+
try {
|
|
2755
|
+
let response = await fetchWrapper(
|
|
2756
|
+
'post',
|
|
2757
|
+
'https://webapi.' + this.#connections[uuid].cameraAPIHost + '/api/dropcams.set_properties',
|
|
2758
|
+
{
|
|
2759
|
+
headers: {
|
|
2760
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
2761
|
+
'User-Agent': USERAGENT,
|
|
2762
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2763
|
+
[this.#connections[uuid].cameraAPI.key]:
|
|
2764
|
+
this.#connections[uuid].cameraAPI.value + this.#connections[uuid].cameraAPI.token,
|
|
2765
|
+
},
|
|
2766
|
+
timeout: NESTAPITIMEOUT,
|
|
2868
2767
|
},
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
error?.cause !== undefined &&
|
|
2882
|
-
JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
|
|
2883
|
-
this?.log?.debug
|
|
2884
|
-
) {
|
|
2885
|
-
this.log.debug(
|
|
2886
|
-
'REST API camera update for failed with error for uuid "%s". Error was "%s"',
|
|
2887
|
-
nest_google_uuid,
|
|
2888
|
-
error?.code,
|
|
2889
|
-
);
|
|
2890
|
-
}
|
|
2891
|
-
});
|
|
2768
|
+
mappedKey + '=' + value + '&uuid=' + nest_google_uuid.split('.')[1],
|
|
2769
|
+
);
|
|
2770
|
+
|
|
2771
|
+
let data = await response.json();
|
|
2772
|
+
if (data?.status !== 0) {
|
|
2773
|
+
throw new Error('Nest API camera update failed');
|
|
2774
|
+
}
|
|
2775
|
+
} catch (error) {
|
|
2776
|
+
if (error?.cause !== undefined && String(error.cause).toUpperCase().includes('TIMEOUT') === false) {
|
|
2777
|
+
this?.log?.debug?.('Nest API camera update failed for uuid "%s". Error was "%s"', nest_google_uuid, error?.code);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2892
2780
|
}),
|
|
2893
2781
|
);
|
|
2894
2782
|
}
|
|
2895
2783
|
|
|
2896
|
-
if (this.#rawData?.[nest_google_uuid]?.source ===
|
|
2784
|
+
if (this.#rawData?.[nest_google_uuid]?.source === DATASOURCE.NestAPI && nest_google_uuid.startsWith('quartz.') === false) {
|
|
2897
2785
|
// set values on other Nest devices besides cameras/doorbells
|
|
2898
2786
|
await Promise.all(
|
|
2899
2787
|
Object.entries(values)
|
|
2900
2788
|
.filter(([key]) => key !== 'uuid')
|
|
2901
2789
|
.map(async ([key, value]) => {
|
|
2902
2790
|
let subscribeJSONData = { objects: [] };
|
|
2791
|
+
let RESTStructureUUID = nest_google_uuid;
|
|
2903
2792
|
|
|
2904
|
-
if (nest_google_uuid.startsWith('
|
|
2905
|
-
|
|
2793
|
+
if (nest_google_uuid.startsWith('kryptonite.') === true) {
|
|
2794
|
+
if (
|
|
2795
|
+
key === 'active_sensor' &&
|
|
2796
|
+
typeof value === 'boolean' &&
|
|
2797
|
+
typeof this.#rawData?.['rcs_settings.' + this.#rawData?.[nest_google_uuid]?.value?.associated_thermostat.split('.')[1]]
|
|
2798
|
+
?.value?.active_rcs_sensors === 'object'
|
|
2799
|
+
) {
|
|
2800
|
+
// Set active temperature sensor for associated thermostat
|
|
2801
|
+
RESTStructureUUID = 'rcs_settings.' + this.#rawData[nest_google_uuid].value.associated_thermostat.split('.')[1];
|
|
2802
|
+
subscribeJSONData.objects.push({
|
|
2803
|
+
object_key: RESTStructureUUID,
|
|
2804
|
+
op: 'MERGE',
|
|
2805
|
+
value:
|
|
2806
|
+
value === true
|
|
2807
|
+
? { active_rcs_sensors: [nest_google_uuid], rcs_control_setting: 'OVERRIDE' }
|
|
2808
|
+
: { active_rcs_sensors: [], rcs_control_setting: 'OFF' },
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2906
2811
|
}
|
|
2907
2812
|
|
|
2908
|
-
// Some elements when setting thermostat data are located in a different object location than with the device object
|
|
2909
|
-
// Handle this scenario below
|
|
2910
2813
|
if (nest_google_uuid.startsWith('device.') === true) {
|
|
2911
|
-
|
|
2912
|
-
|
|
2814
|
+
// Set thermostat settings. Some settings are located in a different ocject location, so we handle this below also
|
|
2913
2815
|
if (
|
|
2914
2816
|
(key === 'hvac_mode' &&
|
|
2915
2817
|
typeof value === 'string' &&
|
|
@@ -2934,39 +2836,47 @@ export default class NestAccfactory {
|
|
|
2934
2836
|
value = value !== 0 ? 'stage' + value : 'stage1';
|
|
2935
2837
|
}
|
|
2936
2838
|
|
|
2839
|
+
if (key === 'hot_water_boost_active' && typeof value === 'object') {
|
|
2840
|
+
key = 'hot_water_boost_time_to_end';
|
|
2841
|
+
value =
|
|
2842
|
+
value?.state === true ? Number(isNaN(value?.time) === false ? value?.time : 30 * 60) + Math.floor(Date.now() / 1000) : 0;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2937
2845
|
subscribeJSONData.objects.push({ object_key: RESTStructureUUID, op: 'MERGE', value: { [key]: value } });
|
|
2938
2846
|
}
|
|
2939
2847
|
|
|
2848
|
+
if (nest_google_uuid.startsWith('device.') === false && nest_google_uuid.startsWith('kryptonite.') === false) {
|
|
2849
|
+
// Set other Nest object settings ie: not thermostat or temperature sensors
|
|
2850
|
+
subscribeJSONData.objects.push({ object_key: nest_google_uuid, op: 'MERGE', value: { [key]: value } });
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2940
2853
|
if (subscribeJSONData.objects.length !== 0) {
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2854
|
+
try {
|
|
2855
|
+
await fetchWrapper(
|
|
2856
|
+
'post',
|
|
2857
|
+
this.#connections[uuid].transport_url + '/v5/put',
|
|
2858
|
+
{
|
|
2859
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
2860
|
+
headers: {
|
|
2861
|
+
'User-Agent': USERAGENT,
|
|
2862
|
+
Authorization: 'Basic ' + this.#connections[uuid].token,
|
|
2863
|
+
},
|
|
2949
2864
|
},
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
this
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
error?.code,
|
|
2958
|
-
);
|
|
2959
|
-
});
|
|
2865
|
+
JSON.stringify(subscribeJSONData),
|
|
2866
|
+
);
|
|
2867
|
+
} catch (error) {
|
|
2868
|
+
if (error?.cause !== undefined && String(error.cause).toUpperCase().includes('TIMEOUT') === false) {
|
|
2869
|
+
this?.log?.debug?.('Nest API property update failed for uuid "%s". Error was "%s"', nest_google_uuid, error?.code);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2960
2872
|
}
|
|
2961
2873
|
}),
|
|
2962
2874
|
);
|
|
2963
2875
|
}
|
|
2964
2876
|
}
|
|
2965
2877
|
|
|
2966
|
-
async #get(
|
|
2878
|
+
async #get(values) {
|
|
2967
2879
|
if (
|
|
2968
|
-
typeof uuid !== 'string' ||
|
|
2969
|
-
uuid === '' ||
|
|
2970
2880
|
typeof values !== 'object' ||
|
|
2971
2881
|
values?.uuid === undefined ||
|
|
2972
2882
|
typeof this.#rawData?.[values?.uuid] !== 'object' ||
|
|
@@ -2976,7 +2886,7 @@ export default class NestAccfactory {
|
|
|
2976
2886
|
}
|
|
2977
2887
|
|
|
2978
2888
|
let nest_google_uuid = values.uuid; // Nest/Google structure uuid for this get request
|
|
2979
|
-
let
|
|
2889
|
+
let uuid = this.#rawData[values.uuid].connection; // Connection uuid for this device
|
|
2980
2890
|
|
|
2981
2891
|
await Promise.all(
|
|
2982
2892
|
Object.entries(values)
|
|
@@ -2987,50 +2897,48 @@ export default class NestAccfactory {
|
|
|
2987
2897
|
values[key] = undefined;
|
|
2988
2898
|
|
|
2989
2899
|
if (
|
|
2990
|
-
this.#rawData?.[nest_google_uuid]?.source ===
|
|
2900
|
+
this.#rawData?.[nest_google_uuid]?.source === DATASOURCE.NestAPI &&
|
|
2991
2901
|
key === 'camera_snapshot' &&
|
|
2992
2902
|
nest_google_uuid.startsWith('quartz.') === true &&
|
|
2993
2903
|
typeof this.#rawData?.[nest_google_uuid]?.value?.nexus_api_http_server_url === 'string' &&
|
|
2994
2904
|
this.#rawData[nest_google_uuid].value.nexus_api_http_server_url !== ''
|
|
2995
2905
|
) {
|
|
2996
|
-
// Attempt to retrieve snapshot from camera via
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
this.#connections[
|
|
2906
|
+
// Attempt to retrieve snapshot from camera via Nest API
|
|
2907
|
+
try {
|
|
2908
|
+
let response = await fetchWrapper(
|
|
2909
|
+
'get',
|
|
2910
|
+
this.#rawData[nest_google_uuid].value.nexus_api_http_server_url + '/get_image?uuid=' + nest_google_uuid.split('.')[1],
|
|
2911
|
+
{
|
|
2912
|
+
headers: {
|
|
2913
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
2914
|
+
'User-Agent': USERAGENT,
|
|
2915
|
+
[this.#connections[uuid].cameraAPI.key]:
|
|
2916
|
+
this.#connections[uuid].cameraAPI.value + this.#connections[uuid].cameraAPI.token,
|
|
2917
|
+
},
|
|
2918
|
+
timeout: 3000,
|
|
3006
2919
|
},
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
)
|
|
3010
|
-
.
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
this?.log?.debug
|
|
3019
|
-
) {
|
|
3020
|
-
this.log.debug('REST API camera snapshot failed with error for uuid "%s". Error was "%s"', nest_google_uuid, error?.code);
|
|
3021
|
-
}
|
|
3022
|
-
});
|
|
2920
|
+
);
|
|
2921
|
+
values[key] = Buffer.from(await response.arrayBuffer());
|
|
2922
|
+
} catch (error) {
|
|
2923
|
+
if (error?.cause !== undefined && String(error.cause).toUpperCase().includes('TIMEOUT') === false) {
|
|
2924
|
+
this?.log?.debug?.(
|
|
2925
|
+
'Nest API camera snapshot failed with error for uuid "%s". Error was "%s"',
|
|
2926
|
+
nest_google_uuid,
|
|
2927
|
+
error?.code,
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
3023
2931
|
}
|
|
3024
2932
|
|
|
3025
2933
|
if (
|
|
3026
|
-
this.#rawData?.[nest_google_uuid]?.source ===
|
|
2934
|
+
this.#rawData?.[nest_google_uuid]?.source === DATASOURCE.ProtobufAPI &&
|
|
3027
2935
|
this.#protobufRoot !== null &&
|
|
3028
2936
|
this.#rawData[nest_google_uuid]?.value?.device_identity?.vendorProductId !== undefined &&
|
|
3029
2937
|
key === 'camera_snapshot'
|
|
3030
2938
|
) {
|
|
3031
2939
|
// Attempt to retrieve snapshot from camera via Protobuf API
|
|
3032
2940
|
// First, request to get snapshot url image updated
|
|
3033
|
-
let commandResponse = await this.#protobufCommand(
|
|
2941
|
+
let commandResponse = await this.#protobufCommand(uuid, 'ResourceApi', 'SendCommand', {
|
|
3034
2942
|
resourceRequest: {
|
|
3035
2943
|
resourceId: nest_google_uuid,
|
|
3036
2944
|
requestId: crypto.randomUUID(),
|
|
@@ -3051,32 +2959,26 @@ export default class NestAccfactory {
|
|
|
3051
2959
|
typeof this.#rawData?.[nest_google_uuid]?.value?.upload_live_image?.liveImageUrl === 'string' &&
|
|
3052
2960
|
this.#rawData[nest_google_uuid].value.upload_live_image.liveImageUrl !== ''
|
|
3053
2961
|
) {
|
|
3054
|
-
// Snapshot url image has
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
.then((response) => response.arrayBuffer())
|
|
3064
|
-
.then((data) => {
|
|
3065
|
-
values[key] = Buffer.from(data);
|
|
3066
|
-
})
|
|
3067
|
-
.catch((error) => {
|
|
3068
|
-
if (
|
|
3069
|
-
error?.cause !== undefined &&
|
|
3070
|
-
JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false &&
|
|
3071
|
-
this?.log?.debug
|
|
3072
|
-
) {
|
|
3073
|
-
this.log.debug(
|
|
3074
|
-
'Protobuf API camera snapshot failed with error for uuid "%s". Error was "%s"',
|
|
3075
|
-
nest_google_uuid,
|
|
3076
|
-
error?.code,
|
|
3077
|
-
);
|
|
3078
|
-
}
|
|
2962
|
+
// Snapshot url image has been updated, so now retrieve it
|
|
2963
|
+
try {
|
|
2964
|
+
let response = await fetchWrapper('get', this.#rawData[nest_google_uuid].value.upload_live_image.liveImageUrl, {
|
|
2965
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
2966
|
+
headers: {
|
|
2967
|
+
'User-Agent': USERAGENT,
|
|
2968
|
+
Authorization: 'Basic ' + this.#connections[uuid].token,
|
|
2969
|
+
},
|
|
2970
|
+
timeout: 3000,
|
|
3079
2971
|
});
|
|
2972
|
+
values[key] = Buffer.from(await response.arrayBuffer());
|
|
2973
|
+
} catch (error) {
|
|
2974
|
+
if (error?.cause !== undefined && String(error.cause).toUpperCase().includes('TIMEOUT') === false) {
|
|
2975
|
+
this?.log?.debug?.(
|
|
2976
|
+
'Protobuf API camera snapshot failed with error for uuid "%s". Error was "%s"',
|
|
2977
|
+
nest_google_uuid,
|
|
2978
|
+
error?.code,
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
3080
2982
|
}
|
|
3081
2983
|
}
|
|
3082
2984
|
}),
|
|
@@ -3085,79 +2987,83 @@ export default class NestAccfactory {
|
|
|
3085
2987
|
return values;
|
|
3086
2988
|
}
|
|
3087
2989
|
|
|
3088
|
-
async #
|
|
3089
|
-
let
|
|
3090
|
-
if (typeof this.#rawData[deviceUUID]?.value?.weather === 'object') {
|
|
3091
|
-
weatherData = this.#rawData?.[deviceUUID]?.value.weather;
|
|
3092
|
-
}
|
|
2990
|
+
async #getWeather(uuid, deviceUUID, latitude, longitude) {
|
|
2991
|
+
let weather = typeof this.#rawData?.[deviceUUID]?.value?.weather === 'object' ? this.#rawData[deviceUUID].value.weather : {};
|
|
3093
2992
|
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
// Store the lat/long details in the weather data object
|
|
3105
|
-
weatherData.latitude = latitude;
|
|
3106
|
-
weatherData.longitude = longitude;
|
|
3107
|
-
|
|
3108
|
-
// Update weather data
|
|
3109
|
-
if (data?.[latitude + ',' + longitude]?.current !== undefined) {
|
|
3110
|
-
weatherData.current_temperature = adjustTemperature(data[latitude + ',' + longitude].current.temp_c, 'C', 'C', false);
|
|
3111
|
-
weatherData.current_humidity = data[latitude + ',' + longitude].current.humidity;
|
|
3112
|
-
weatherData.condition = data[latitude + ',' + longitude].current.condition;
|
|
3113
|
-
weatherData.wind_direction = data[latitude + ',' + longitude].current.wind_dir;
|
|
3114
|
-
weatherData.wind_speed = data[latitude + ',' + longitude].current.wind_mph * 1.609344; // convert to km/h
|
|
3115
|
-
weatherData.sunrise = data[latitude + ',' + longitude].current.sunrise;
|
|
3116
|
-
weatherData.sunset = data[latitude + ',' + longitude].current.sunset;
|
|
3117
|
-
}
|
|
3118
|
-
weatherData.station =
|
|
3119
|
-
data[latitude + ',' + longitude]?.location?.short_name !== undefined
|
|
3120
|
-
? data[latitude + ',' + longitude].location.short_name
|
|
3121
|
-
: '';
|
|
3122
|
-
weatherData.forecast =
|
|
3123
|
-
data[latitude + ',' + longitude]?.forecast?.daily?.[0]?.condition !== undefined
|
|
3124
|
-
? data[latitude + ',' + longitude].forecast.daily[0].condition
|
|
3125
|
-
: '';
|
|
3126
|
-
})
|
|
3127
|
-
.catch((error) => {
|
|
3128
|
-
if (error?.cause !== undefined && JSON.stringify(error.cause).toUpperCase().includes('TIMEOUT') === false && this?.log?.debug) {
|
|
3129
|
-
this.log.debug('REST API failed to retrieve weather details for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
3130
|
-
}
|
|
2993
|
+
let location = latitude + ',' + longitude;
|
|
2994
|
+
|
|
2995
|
+
if (typeof this.#connections?.[uuid]?.weather_url === 'string' && this.#connections[uuid].weather_url !== '') {
|
|
2996
|
+
try {
|
|
2997
|
+
let response = await fetchWrapper('get', this.#connections[uuid].weather_url + location, {
|
|
2998
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
2999
|
+
headers: {
|
|
3000
|
+
'User-Agent': USERAGENT,
|
|
3001
|
+
},
|
|
3002
|
+
timeout: NESTAPITIMEOUT,
|
|
3131
3003
|
});
|
|
3004
|
+
|
|
3005
|
+
let data = await response.json();
|
|
3006
|
+
let locationData = data?.[location];
|
|
3007
|
+
|
|
3008
|
+
// Store the lat/long details in the weather data object
|
|
3009
|
+
weather.latitude = latitude;
|
|
3010
|
+
weather.longitude = longitude;
|
|
3011
|
+
|
|
3012
|
+
// Update weather data
|
|
3013
|
+
if (locationData?.current !== undefined) {
|
|
3014
|
+
weather.current_temperature = adjustTemperature(locationData.current.temp_c, 'C', 'C', false);
|
|
3015
|
+
weather.current_humidity = locationData.current.humidity;
|
|
3016
|
+
weather.condition = locationData.current.condition;
|
|
3017
|
+
weather.wind_direction = locationData.current.wind_dir;
|
|
3018
|
+
weather.wind_speed = locationData.current.wind_mph * 1.609344; // convert to km/h
|
|
3019
|
+
weather.sunrise = locationData.current.sunrise;
|
|
3020
|
+
weather.sunset = locationData.current.sunset;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
weather.station = typeof locationData?.location?.short_name === 'string' ? locationData.location.short_name : '';
|
|
3024
|
+
weather.forecast = locationData?.forecast?.daily?.[0]?.condition !== undefined ? locationData.forecast.daily[0].condition : '';
|
|
3025
|
+
} catch (error) {
|
|
3026
|
+
if (error?.cause !== undefined && String(error.cause).toUpperCase().includes('TIMEOUT') === false) {
|
|
3027
|
+
this?.log?.debug?.('Nest API failed to retrieve weather details for uuid "%s". Error was "%s"', deviceUUID, error?.code);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3132
3030
|
}
|
|
3133
3031
|
|
|
3134
|
-
return
|
|
3032
|
+
return weather;
|
|
3135
3033
|
}
|
|
3136
3034
|
|
|
3137
|
-
async #protobufCommand(
|
|
3035
|
+
async #protobufCommand(uuid, service, command, values) {
|
|
3138
3036
|
if (
|
|
3139
3037
|
this.#protobufRoot === null ||
|
|
3038
|
+
typeof uuid !== 'string' ||
|
|
3039
|
+
!uuid ||
|
|
3140
3040
|
typeof service !== 'string' ||
|
|
3141
|
-
service
|
|
3041
|
+
!service ||
|
|
3142
3042
|
typeof command !== 'string' ||
|
|
3143
|
-
command
|
|
3144
|
-
typeof values !== 'object'
|
|
3043
|
+
!command ||
|
|
3044
|
+
typeof values !== 'object' ||
|
|
3045
|
+
values === null ||
|
|
3046
|
+
this.#connections?.[uuid] === undefined ||
|
|
3047
|
+
typeof this.#connections?.[uuid].protobufAPIHost !== 'string' ||
|
|
3048
|
+
typeof this.#connections?.[uuid].referer !== 'string' ||
|
|
3049
|
+
typeof this.#connections?.[uuid].token !== 'string'
|
|
3145
3050
|
) {
|
|
3146
3051
|
return;
|
|
3147
3052
|
}
|
|
3148
3053
|
|
|
3149
3054
|
const encodeValues = (object) => {
|
|
3150
3055
|
if (typeof object === 'object' && object !== null) {
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
let
|
|
3056
|
+
// We have a type_url and value object at this same level, we'll treat this a trait requiring encoding
|
|
3057
|
+
if (typeof object.type_url === 'string' && object.value !== undefined) {
|
|
3058
|
+
let typeName = object.type_url.split('/')[1];
|
|
3059
|
+
let TraitMap = this.#protobufRoot.lookup(typeName);
|
|
3154
3060
|
if (TraitMap !== null) {
|
|
3155
3061
|
object.value = TraitMap.encode(TraitMap.fromObject(object.value)).finish();
|
|
3156
3062
|
}
|
|
3157
3063
|
}
|
|
3158
3064
|
|
|
3159
|
-
for (
|
|
3160
|
-
if (object
|
|
3065
|
+
for (const key in object) {
|
|
3066
|
+
if (object[key] !== undefined) {
|
|
3161
3067
|
encodeValues(object[key]);
|
|
3162
3068
|
}
|
|
3163
3069
|
}
|
|
@@ -3174,28 +3080,27 @@ export default class NestAccfactory {
|
|
|
3174
3080
|
encodeValues(values);
|
|
3175
3081
|
|
|
3176
3082
|
let encodedData = TraitMapRequest.encode(TraitMapRequest.fromObject(values)).finish();
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3083
|
+
try {
|
|
3084
|
+
let response = await fetchWrapper(
|
|
3085
|
+
'post',
|
|
3086
|
+
'https://' + this.#connections[uuid].protobufAPIHost + '/nestlabs.gateway.v1.' + service + '/' + command,
|
|
3087
|
+
{
|
|
3088
|
+
headers: {
|
|
3089
|
+
referer: 'https://' + this.#connections[uuid].referer,
|
|
3090
|
+
'User-Agent': USERAGENT,
|
|
3091
|
+
Authorization: 'Basic ' + this.#connections[uuid].token,
|
|
3092
|
+
'Content-Type': 'application/x-protobuf',
|
|
3093
|
+
'X-Accept-Content-Transfer-Encoding': 'binary',
|
|
3094
|
+
'X-Accept-Response-Streaming': 'true',
|
|
3095
|
+
},
|
|
3188
3096
|
},
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
.
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
.catch((error) => {
|
|
3197
|
-
this?.log?.debug && this.log.debug('Protobuf gateway service command failed with error. Error was "%s"', error?.code);
|
|
3198
|
-
});
|
|
3097
|
+
encodedData,
|
|
3098
|
+
);
|
|
3099
|
+
let buffer = await response.arrayBuffer();
|
|
3100
|
+
commandResponse = TraitMapResponse.decode(Buffer.from(buffer)).toJSON();
|
|
3101
|
+
} catch (error) {
|
|
3102
|
+
this?.log?.debug?.('Protobuf gateway service command failed with error. Error was "%s"', error?.code);
|
|
3103
|
+
}
|
|
3199
3104
|
}
|
|
3200
3105
|
return commandResponse;
|
|
3201
3106
|
}
|
|
@@ -3203,28 +3108,22 @@ export default class NestAccfactory {
|
|
|
3203
3108
|
|
|
3204
3109
|
// General helper functions which don't need to be part of an object class
|
|
3205
3110
|
function adjustTemperature(temperature, currentTemperatureUnit, targetTemperatureUnit, round) {
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
if (targetTemperatureUnit.toUpperCase() === 'C') {
|
|
3209
|
-
if (currentTemperatureUnit.toUpperCase() === 'F') {
|
|
3210
|
-
// convert from F to C
|
|
3211
|
-
temperature = ((temperature - 32) * 5) / 9;
|
|
3212
|
-
}
|
|
3213
|
-
if (round === true) {
|
|
3214
|
-
// round to nearest 0.5C
|
|
3215
|
-
temperature = Math.round(temperature * 2) * 0.5;
|
|
3216
|
-
}
|
|
3217
|
-
}
|
|
3111
|
+
currentTemperatureUnit = currentTemperatureUnit?.toUpperCase?.();
|
|
3112
|
+
targetTemperatureUnit = targetTemperatureUnit?.toUpperCase?.();
|
|
3218
3113
|
|
|
3219
|
-
if (
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
temperature = (temperature *
|
|
3114
|
+
if (currentTemperatureUnit === 'F' && targetTemperatureUnit === 'C') {
|
|
3115
|
+
temperature = ((temperature - 32) * 5) / 9;
|
|
3116
|
+
if (round === true) {
|
|
3117
|
+
temperature = Math.round(temperature * 2) / 2; // round to nearest 0.5°C
|
|
3223
3118
|
}
|
|
3119
|
+
} else if (currentTemperatureUnit === 'C' && targetTemperatureUnit === 'F') {
|
|
3120
|
+
temperature = (temperature * 9) / 5 + 32;
|
|
3224
3121
|
if (round === true) {
|
|
3225
|
-
// round to nearest
|
|
3226
|
-
temperature = Math.round(temperature);
|
|
3122
|
+
temperature = Math.round(temperature); // round to nearest 1°F
|
|
3227
3123
|
}
|
|
3124
|
+
} else if (round === true) {
|
|
3125
|
+
// No conversion, just rounding
|
|
3126
|
+
temperature = targetTemperatureUnit === 'C' ? Math.round(temperature * 2) / 2 : Math.round(temperature);
|
|
3228
3127
|
}
|
|
3229
3128
|
|
|
3230
3129
|
return temperature;
|
|
@@ -3234,12 +3133,14 @@ function makeHomeKitName(nameToMakeValid) {
|
|
|
3234
3133
|
// Strip invalid characters to meet HomeKit naming requirements
|
|
3235
3134
|
// Ensure only letters or numbers are at the beginning AND/OR end of string
|
|
3236
3135
|
// Matches against uni-code characters
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3136
|
+
let validHomeKitName = nameToMakeValid;
|
|
3137
|
+
if (typeof nameToMakeValid === 'string') {
|
|
3138
|
+
validHomeKitName = nameToMakeValid
|
|
3139
|
+
.replace(/[^\p{L}\p{N}\p{Z}\u2019 '.,-]/gu, '') // Remove disallowed characters
|
|
3140
|
+
.replace(/^[^\p{L}\p{N}]*/gu, '') // Trim invalid prefix
|
|
3141
|
+
.replace(/[^\p{L}\p{N}]+$/gu, ''); // Trim invalid suffix
|
|
3142
|
+
}
|
|
3143
|
+
return validHomeKitName;
|
|
3243
3144
|
}
|
|
3244
3145
|
|
|
3245
3146
|
function crc24(valueToHash) {
|
|
@@ -3266,65 +3167,127 @@ function crc24(valueToHash) {
|
|
|
3266
3167
|
0x7c0401, 0x42fa2f, 0xc4b6d4, 0xc82f22, 0x4e63d9, 0xd11cce, 0x575035, 0x5bc9c3, 0xdd8538,
|
|
3267
3168
|
];
|
|
3268
3169
|
|
|
3269
|
-
let
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3170
|
+
let crc = 0xb704ce;
|
|
3171
|
+
|
|
3172
|
+
const buffer = Buffer.from(valueToHash);
|
|
3173
|
+
|
|
3174
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
3175
|
+
const index = ((crc >> 16) ^ buffer[i]) & 0xff;
|
|
3176
|
+
crc = (crc24HashTable[index] ^ (crc << 8)) & 0xffffff;
|
|
3273
3177
|
}
|
|
3274
|
-
|
|
3178
|
+
|
|
3179
|
+
return crc.toString(16).padStart(6, '0'); // ensures 6-digit hex
|
|
3275
3180
|
}
|
|
3276
3181
|
|
|
3277
|
-
function scaleValue(value,
|
|
3278
|
-
if (
|
|
3279
|
-
|
|
3182
|
+
function scaleValue(value, sourceMin, sourceMax, targetMin, targetMax) {
|
|
3183
|
+
if (sourceMax === sourceMin) {
|
|
3184
|
+
return targetMin;
|
|
3280
3185
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
return ((value -
|
|
3186
|
+
|
|
3187
|
+
value = Math.max(sourceMin, Math.min(sourceMax, value));
|
|
3188
|
+
|
|
3189
|
+
return ((value - sourceMin) * (targetMax - targetMin)) / (sourceMax - sourceMin) + targetMin;
|
|
3285
3190
|
}
|
|
3286
3191
|
|
|
3287
|
-
async function fetchWrapper(method, url, options, data
|
|
3192
|
+
async function fetchWrapper(method, url, options, data) {
|
|
3288
3193
|
if ((method !== 'get' && method !== 'post') || typeof url !== 'string' || url === '' || typeof options !== 'object') {
|
|
3289
3194
|
return;
|
|
3290
3195
|
}
|
|
3291
3196
|
|
|
3292
|
-
if (isNaN(options?.timeout) === false && Number(options
|
|
3293
|
-
// If a timeout is specified in the options, setup here
|
|
3197
|
+
if (isNaN(options?.timeout) === false && Number(options.timeout) > 0) {
|
|
3294
3198
|
// eslint-disable-next-line no-undef
|
|
3295
3199
|
options.signal = AbortSignal.timeout(Number(options.timeout));
|
|
3296
3200
|
}
|
|
3297
3201
|
|
|
3298
|
-
if (options
|
|
3299
|
-
// If not retry option specifed , we'll do just once
|
|
3202
|
+
if (isNaN(options.retry) === true || options.retry < 1) {
|
|
3300
3203
|
options.retry = 1;
|
|
3301
3204
|
}
|
|
3302
3205
|
|
|
3303
|
-
options.
|
|
3206
|
+
if (isNaN(options._retryCount) === true) {
|
|
3207
|
+
options._retryCount = 0;
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
options.method = method;
|
|
3304
3211
|
|
|
3305
|
-
if (method === 'post' &&
|
|
3306
|
-
// Doing a HTTP post, so include the data in the body
|
|
3212
|
+
if (method === 'post' && data !== undefined) {
|
|
3307
3213
|
options.body = data;
|
|
3308
3214
|
}
|
|
3309
3215
|
|
|
3310
|
-
|
|
3216
|
+
let response;
|
|
3217
|
+
try {
|
|
3311
3218
|
// eslint-disable-next-line no-undef
|
|
3312
3219
|
response = await fetch(url, options);
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3220
|
+
} catch (error) {
|
|
3221
|
+
if (options.retry > 1) {
|
|
3222
|
+
options.retry--;
|
|
3223
|
+
options._retryCount++;
|
|
3224
|
+
|
|
3225
|
+
const delay = 500 * 2 ** (options._retryCount - 1);
|
|
3226
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3227
|
+
|
|
3228
|
+
return fetchWrapper(method, url, options, data);
|
|
3321
3229
|
}
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3230
|
+
|
|
3231
|
+
error.message = `Fetch failed for ${method.toUpperCase()} ${url} after ${options._retryCount + 1} attempt(s): ${error.message}`;
|
|
3232
|
+
throw error;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
if (response?.ok === false) {
|
|
3236
|
+
if (options.retry > 1) {
|
|
3237
|
+
options.retry--;
|
|
3238
|
+
options._retryCount++;
|
|
3239
|
+
|
|
3240
|
+
let delay = 500 * 2 ** (options._retryCount - 1);
|
|
3241
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3242
|
+
|
|
3243
|
+
return fetchWrapper(method, url, options, data);
|
|
3326
3244
|
}
|
|
3245
|
+
|
|
3246
|
+
let error = new Error(`HTTP ${response.status} on ${method.toUpperCase()} ${url}: ${response.statusText || 'Unknown error'}`);
|
|
3247
|
+
error.code = response.status;
|
|
3248
|
+
throw error;
|
|
3327
3249
|
}
|
|
3328
3250
|
|
|
3329
3251
|
return response;
|
|
3330
3252
|
}
|
|
3253
|
+
|
|
3254
|
+
function parseDurationToSeconds(inputDuration, { defaultValue = null, min = 0, max = Infinity } = {}) {
|
|
3255
|
+
let normalisedSeconds = defaultValue;
|
|
3256
|
+
|
|
3257
|
+
if (inputDuration !== undefined && inputDuration !== null && inputDuration !== '') {
|
|
3258
|
+
inputDuration = String(inputDuration).trim().toLowerCase();
|
|
3259
|
+
|
|
3260
|
+
// Case: plain numeric seconds (e.g. "30")
|
|
3261
|
+
if (/^\d+$/.test(inputDuration) === true) {
|
|
3262
|
+
normalisedSeconds = Number(inputDuration);
|
|
3263
|
+
} else {
|
|
3264
|
+
// Process input into normalised units. We'll convert in standard h (hours), m (minutes), s (seconds)
|
|
3265
|
+
inputDuration = inputDuration
|
|
3266
|
+
.replace(/hrs?|hours?/g, 'h')
|
|
3267
|
+
.replace(/mins?|minutes?/g, 'm')
|
|
3268
|
+
.replace(/secs?|s\b/g, 's')
|
|
3269
|
+
.replace(/ +/g, '');
|
|
3270
|
+
|
|
3271
|
+
// Match duration format like "1h30m15s"
|
|
3272
|
+
let match = inputDuration.match(/^((\d+)h)?((\d+)m)?((\d+)s?)?$/);
|
|
3273
|
+
|
|
3274
|
+
if (Array.isArray(match) === true) {
|
|
3275
|
+
let total = Number(match[2] || 0) * 3600 + Number(match[4] || 0) * 60 + Number(match[6] || 0);
|
|
3276
|
+
normalisedSeconds = Math.floor(total / 3600) * 3600 + Math.floor((total % 3600) / 60) * 60 + (total % 60);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
if (normalisedSeconds === null || isNaN(normalisedSeconds) === true) {
|
|
3281
|
+
normalisedSeconds = defaultValue;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
if (isNaN(min) === false && normalisedSeconds < min) {
|
|
3285
|
+
normalisedSeconds = min;
|
|
3286
|
+
}
|
|
3287
|
+
if (isNaN(max) === false && normalisedSeconds > max) {
|
|
3288
|
+
normalisedSeconds = max;
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
return normalisedSeconds;
|
|
3293
|
+
}
|