homebridge-nuheat2 1.2.4-beta.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 +89 -0
- package/README.md +120 -0
- package/config.schema.json +101 -0
- package/index.js +360 -0
- package/lib/NuHeatAPI.js +696 -0
- package/lib/NuHeatGroup.js +74 -0
- package/lib/NuHeatListener.js +133 -0
- package/lib/NuHeatThermostat.js +247 -0
- package/lib/logger.js +35 -0
- package/lib/settings.js +19 -0
- package/package.json +51 -0
package/lib/NuHeatAPI.js
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const {
|
|
3
|
+
default: fetch,
|
|
4
|
+
FetchError,
|
|
5
|
+
Headers,
|
|
6
|
+
RequestInfo,
|
|
7
|
+
RequestInit,
|
|
8
|
+
Response,
|
|
9
|
+
isRedirect,
|
|
10
|
+
} = require("node-fetch-cjs");
|
|
11
|
+
const { parse } = require("node-html-parser");
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
NUHEAT_API_CLIENT_ID,
|
|
15
|
+
NUHEAT_API_CLIENT_SECRET,
|
|
16
|
+
NUHEAT_API_REDIRECT_URI,
|
|
17
|
+
NUHEAT_API_AUTHORIZE_URI,
|
|
18
|
+
NUHEAT_API_TOKEN_URI,
|
|
19
|
+
NUHEAT_API_CONSENT_URI,
|
|
20
|
+
} = require("./settings");
|
|
21
|
+
|
|
22
|
+
module.exports = class NuHeatAPI {
|
|
23
|
+
constructor(email, password, log, options = {}) {
|
|
24
|
+
this.email = email;
|
|
25
|
+
this.password = password;
|
|
26
|
+
this.log = log;
|
|
27
|
+
this.oauthClientId =
|
|
28
|
+
options.clientId ||
|
|
29
|
+
process.env.NUHEAT_API_CLIENT_ID ||
|
|
30
|
+
NUHEAT_API_CLIENT_ID;
|
|
31
|
+
this.oauthClientSecret =
|
|
32
|
+
options.clientSecret ||
|
|
33
|
+
process.env.NUHEAT_API_CLIENT_SECRET ||
|
|
34
|
+
NUHEAT_API_CLIENT_SECRET;
|
|
35
|
+
this.oauthRedirectUri =
|
|
36
|
+
options.redirectUri ||
|
|
37
|
+
process.env.NUHEAT_API_REDIRECT_URI ||
|
|
38
|
+
NUHEAT_API_REDIRECT_URI;
|
|
39
|
+
this.usingFallbackCredentials =
|
|
40
|
+
!options.clientId &&
|
|
41
|
+
!process.env.NUHEAT_API_CLIENT_ID &&
|
|
42
|
+
!options.clientSecret &&
|
|
43
|
+
!process.env.NUHEAT_API_CLIENT_SECRET;
|
|
44
|
+
this.headers = new Headers();
|
|
45
|
+
this.headers.set("Content-Type", "application/json");
|
|
46
|
+
this.headers.set("Accept", "application/json");
|
|
47
|
+
|
|
48
|
+
if (this.usingFallbackCredentials) {
|
|
49
|
+
this.log.warn(
|
|
50
|
+
"NuHeatAPI: Using built-in OAuth client credentials. Request your own Nuheat API client for long-term reliability.",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Set the away mode of a group
|
|
56
|
+
async setAwayMode(groupId, awayMode) {
|
|
57
|
+
// Set the URL for the put call
|
|
58
|
+
const callURL = "https://api.mynuheat.com/api/v1/Group";
|
|
59
|
+
|
|
60
|
+
// Create the request to change away mode.
|
|
61
|
+
const callBody = JSON.stringify({
|
|
62
|
+
groupId,
|
|
63
|
+
awayMode,
|
|
64
|
+
});
|
|
65
|
+
const callOptions = {
|
|
66
|
+
body: callBody,
|
|
67
|
+
method: "PUT",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let returnedData = await this.makeAPICall(callURL, callOptions);
|
|
71
|
+
return returnedData;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Set the setpoint of a thermostat
|
|
75
|
+
async setHeatSetpoint(serialNumber, setPointTemp, holdLength) {
|
|
76
|
+
let scheduleMode = 3;
|
|
77
|
+
let holdSetPointDateTime;
|
|
78
|
+
if (holdLength < 1440) {
|
|
79
|
+
scheduleMode = 2;
|
|
80
|
+
if (holdLength > 0) {
|
|
81
|
+
holdSetPointDateTime =
|
|
82
|
+
new Date(Date.now() + holdLength * 60 * 1000)
|
|
83
|
+
.toISOString()
|
|
84
|
+
.split(".")[0]
|
|
85
|
+
.toString() + "Z";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Set the URL for the put call
|
|
90
|
+
const callURL = "https://api.mynuheat.com/api/v1/Thermostat";
|
|
91
|
+
|
|
92
|
+
/// Create the request to change setpoint
|
|
93
|
+
let callBody = {
|
|
94
|
+
serialNumber,
|
|
95
|
+
setPointTemp,
|
|
96
|
+
scheduleMode,
|
|
97
|
+
};
|
|
98
|
+
if (holdSetPointDateTime) {
|
|
99
|
+
callBody.holdSetPointDateTime = holdSetPointDateTime;
|
|
100
|
+
}
|
|
101
|
+
this.log.info(JSON.stringify(callBody));
|
|
102
|
+
const callOptions = {
|
|
103
|
+
body: JSON.stringify(callBody),
|
|
104
|
+
method: "PUT",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let returnedData = await this.makeAPICall(callURL, callOptions);
|
|
108
|
+
return returnedData;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// get data for a group
|
|
112
|
+
async refreshGroup(groupId) {
|
|
113
|
+
// set the URL for the call
|
|
114
|
+
const callURL = "https://api.mynuheat.com/api/v1/Group/" + groupId;
|
|
115
|
+
|
|
116
|
+
let returnedData = await this.makeAPICall(callURL);
|
|
117
|
+
return returnedData;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// get data for all groups
|
|
121
|
+
async refreshGroups() {
|
|
122
|
+
// set the URL for the call
|
|
123
|
+
const callURL = "https://api.mynuheat.com/api/v1/Group";
|
|
124
|
+
|
|
125
|
+
let returnedData = await this.makeAPICall(callURL);
|
|
126
|
+
return returnedData;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// get data for a thermostat
|
|
130
|
+
async refreshThermostat(serialNumber) {
|
|
131
|
+
// set the URL for the call
|
|
132
|
+
const callURL =
|
|
133
|
+
"https://api.mynuheat.com/api/v1/Thermostat/" + serialNumber;
|
|
134
|
+
|
|
135
|
+
let returnedData = await this.makeAPICall(callURL);
|
|
136
|
+
return returnedData;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// get data for all thermostat
|
|
140
|
+
async refreshThermostats() {
|
|
141
|
+
// set the URL for the call
|
|
142
|
+
const callURL = "https://api.mynuheat.com/api/v1/Thermostat";
|
|
143
|
+
|
|
144
|
+
let returnedData = await this.makeAPICall(callURL);
|
|
145
|
+
return returnedData;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async makeAPICall(callURL, callOptions = {}) {
|
|
149
|
+
// Validate and potentially refresh our access token.
|
|
150
|
+
if (!(await this.refreshAccessToken())) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
// Execute the refresh token request.
|
|
154
|
+
const response = await this.fetch(callURL, callOptions);
|
|
155
|
+
if (!response) {
|
|
156
|
+
this.log.debug(
|
|
157
|
+
"NuHeatAPI: Unable to make API call. Acquiring a new access token.",
|
|
158
|
+
);
|
|
159
|
+
this.accessToken = null;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
//handle PUT successes that don't return a body
|
|
163
|
+
if (response.status === 204) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
let returnedData = await response.json();
|
|
167
|
+
return returnedData;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Retrieve the NuHeat OAuth authorization page to prepare to login.
|
|
171
|
+
async oauthGetAuthPage() {
|
|
172
|
+
const authEndpoint = new URL(NUHEAT_API_AUTHORIZE_URI);
|
|
173
|
+
|
|
174
|
+
// Set the response type.
|
|
175
|
+
authEndpoint.searchParams.set("response_type", "code");
|
|
176
|
+
|
|
177
|
+
// Set the client identifier.
|
|
178
|
+
authEndpoint.searchParams.set("client_id", this.oauthClientId);
|
|
179
|
+
|
|
180
|
+
// Set the redirect URI to the github page.
|
|
181
|
+
authEndpoint.searchParams.set("redirect_uri", this.oauthRedirectUri);
|
|
182
|
+
|
|
183
|
+
// Set the scope.
|
|
184
|
+
authEndpoint.searchParams.set("scope", "openapi openid offline_access");
|
|
185
|
+
|
|
186
|
+
// Let's begin the login process.
|
|
187
|
+
const response = await this.fetch(authEndpoint.toString(), {
|
|
188
|
+
redirect: "follow",
|
|
189
|
+
});
|
|
190
|
+
if (!response) {
|
|
191
|
+
this.log.error(
|
|
192
|
+
"NuHeatAPI: Unable to access the OAuth authorization endpoint.",
|
|
193
|
+
);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return response;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Login to the NuHeat API, using the retrieved authorization page.
|
|
201
|
+
async oauthLogin(authPage) {
|
|
202
|
+
// Grab the cookie for the OAuth sequence. We need to deal with spurious additions to the cookie that gets returned by the NuHeat API.
|
|
203
|
+
const cookie = this.trimSetCookie(authPage.headers.raw()["set-cookie"]);
|
|
204
|
+
|
|
205
|
+
if (cookie) {
|
|
206
|
+
// Parse the NuHeat login page and grab what we need.
|
|
207
|
+
const htmlText = await authPage.text();
|
|
208
|
+
const loginPageHtml = parse(htmlText);
|
|
209
|
+
|
|
210
|
+
const requestVerificationToken = loginPageHtml
|
|
211
|
+
.querySelector("input[name=__RequestVerificationToken]")
|
|
212
|
+
?.getAttribute("value");
|
|
213
|
+
const requestReturnURL = loginPageHtml
|
|
214
|
+
.querySelector("input[name=ReturnUrl]")
|
|
215
|
+
?.getAttribute("value");
|
|
216
|
+
|
|
217
|
+
if (!requestVerificationToken) {
|
|
218
|
+
this.log.error(
|
|
219
|
+
"NuHeatAPI: Unable to complete OAuth login. The verification token could not be retrieved.",
|
|
220
|
+
);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Set the login info.
|
|
225
|
+
const loginBody = new URLSearchParams({
|
|
226
|
+
ReturnUrl: requestReturnURL,
|
|
227
|
+
Username: this.email,
|
|
228
|
+
Password: this.password,
|
|
229
|
+
button: "login",
|
|
230
|
+
__RequestVerificationToken: requestVerificationToken,
|
|
231
|
+
});
|
|
232
|
+
// Login and we're done.
|
|
233
|
+
const response = await this.fetch(authPage.url, {
|
|
234
|
+
body: loginBody.toString(),
|
|
235
|
+
headers: {
|
|
236
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
237
|
+
Cookie: cookie,
|
|
238
|
+
},
|
|
239
|
+
method: "POST",
|
|
240
|
+
redirect: "manual",
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// An error occurred and we didn't get a good response.
|
|
244
|
+
if (!response) {
|
|
245
|
+
this.log.error(
|
|
246
|
+
"NuHeatAPI: Unable to complete OAuth login. Ensure your username and password are correct.",
|
|
247
|
+
);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If we don't have the full set of cookies we expect, the user probably gave bad login information.
|
|
252
|
+
if (
|
|
253
|
+
response.headers &&
|
|
254
|
+
response.headers.raw()["set-cookie"] &&
|
|
255
|
+
response.headers.raw()["set-cookie"].length < 2
|
|
256
|
+
) {
|
|
257
|
+
this.log.error(
|
|
258
|
+
"NuHeatAPI: Invalid NuHeat credentials given. Check your login and password.",
|
|
259
|
+
);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return response;
|
|
263
|
+
} else {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Confirm homebridge-nuheat access to account
|
|
269
|
+
async oauthConfirm(authPage, sessionCookie) {
|
|
270
|
+
// Get the location for the redirect for later use.
|
|
271
|
+
let redirectUrl = new URL(authPage.headers.get("location"), authPage.url);
|
|
272
|
+
|
|
273
|
+
// Execute the redirect with the cleaned up cookies and we're done.
|
|
274
|
+
let confirmPage = await this.fetch(redirectUrl.toString(), {
|
|
275
|
+
headers: {
|
|
276
|
+
Cookie: sessionCookie,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
if (!confirmPage) {
|
|
280
|
+
this.log.error("NuHeatAPI: Unable to complete the OAuth login redirect.");
|
|
281
|
+
}
|
|
282
|
+
// Grab the cookie for the OAuth sequence. We need to deal with spurious additions to the cookie that gets returned by the NuHeat API.
|
|
283
|
+
let cookie = this.trimSetCookie(confirmPage.headers.raw()["set-cookie"]);
|
|
284
|
+
|
|
285
|
+
if (cookie) {
|
|
286
|
+
// Parse the NuHeat login page and grab what we need.
|
|
287
|
+
const htmlText = await confirmPage.text();
|
|
288
|
+
const loginPageHtml = parse(htmlText);
|
|
289
|
+
|
|
290
|
+
const requestVerificationToken = loginPageHtml
|
|
291
|
+
.querySelector("input[name=__RequestVerificationToken]")
|
|
292
|
+
?.getAttribute("value");
|
|
293
|
+
const requestReturnURL = loginPageHtml
|
|
294
|
+
.querySelector("input[name=ReturnUrl]")
|
|
295
|
+
?.getAttribute("value");
|
|
296
|
+
|
|
297
|
+
if (!requestVerificationToken) {
|
|
298
|
+
this.log.error(
|
|
299
|
+
"NuHeatAPI: Unable to complete OAuth login. The api access couldn't be confirmed.",
|
|
300
|
+
);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Set the login info.
|
|
305
|
+
const loginBody = new URLSearchParams({
|
|
306
|
+
ReturnUrl: requestReturnURL,
|
|
307
|
+
button: "yes",
|
|
308
|
+
RememberConsent: "true",
|
|
309
|
+
__RequestVerificationToken: requestVerificationToken,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Login and we're done.
|
|
313
|
+
let response = await this.fetch(NUHEAT_API_CONSENT_URI, {
|
|
314
|
+
body:
|
|
315
|
+
loginBody.toString() +
|
|
316
|
+
"&ScopesConsented=openid&ScopesConsented=openapi&ScopesConsented=offline_access",
|
|
317
|
+
headers: {
|
|
318
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
319
|
+
Cookie: cookie + "; " + sessionCookie,
|
|
320
|
+
},
|
|
321
|
+
method: "POST",
|
|
322
|
+
redirect: "manual",
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// An error occurred and we didn't get a good response.
|
|
326
|
+
if (!response) {
|
|
327
|
+
this.log.error(
|
|
328
|
+
"NuHeatAPI: Unable to complete OAuth login. API access confirmation not completed.",
|
|
329
|
+
);
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return response;
|
|
334
|
+
} else {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Intercept the OAuth login response to adjust cookie headers before sending on it's way.
|
|
340
|
+
async oauthRedirect(loginResponse, sessionCookie) {
|
|
341
|
+
// Get the location for the redirect for later use.
|
|
342
|
+
const redirectUrl = new URL(
|
|
343
|
+
loginResponse.headers.get("location"),
|
|
344
|
+
NUHEAT_API_AUTHORIZE_URI,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Cleanup the cookie so we can complete the login process by removing spurious additions
|
|
348
|
+
// to the cookie that gets returned by the NuHeat API.
|
|
349
|
+
const cookie = this.trimSetCookie(
|
|
350
|
+
loginResponse.headers.raw()["set-cookie"],
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
if (cookie) {
|
|
354
|
+
// Execute the redirect with the cleaned up cookies and we're done.
|
|
355
|
+
const response = await this.fetch(redirectUrl.toString(), {
|
|
356
|
+
headers: {
|
|
357
|
+
Cookie: cookie + "; " + sessionCookie,
|
|
358
|
+
},
|
|
359
|
+
redirect: "manual",
|
|
360
|
+
});
|
|
361
|
+
if (!response) {
|
|
362
|
+
this.log.error(
|
|
363
|
+
"NuHeatAPI: Unable to complete the OAuth login redirect.",
|
|
364
|
+
);
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
return response;
|
|
368
|
+
} else {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Get a new OAuth access token.
|
|
374
|
+
async getAccessToken() {
|
|
375
|
+
// Call the NuHeat authorization endpoint to get the web login page.
|
|
376
|
+
let response = await this.oauthGetAuthPage();
|
|
377
|
+
|
|
378
|
+
if (!response) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Attempt to login.
|
|
383
|
+
response = await this.oauthLogin(response);
|
|
384
|
+
|
|
385
|
+
if (!response) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Grab the session cookie being used so we can keep using it
|
|
390
|
+
let sessionCookie = this.trimSetCookie(
|
|
391
|
+
response.headers.raw()["set-cookie"],
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (sessionCookie) {
|
|
395
|
+
if (
|
|
396
|
+
response.headers &&
|
|
397
|
+
response.headers
|
|
398
|
+
.get("location")
|
|
399
|
+
.startsWith("/connect/authorize/callback?")
|
|
400
|
+
) {
|
|
401
|
+
// Attempt to confirm api access.
|
|
402
|
+
response = await this.oauthConfirm(response, sessionCookie);
|
|
403
|
+
|
|
404
|
+
if (!response) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Intercept the redirect back to localhost
|
|
410
|
+
response = await this.oauthRedirect(response, sessionCookie);
|
|
411
|
+
if (!response) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Parse the redirect URL to extract the redirect url.
|
|
416
|
+
const redirectUrl = new URL(response.headers.get("location") ?? "");
|
|
417
|
+
|
|
418
|
+
// Create the request to get our access and refresh tokens.
|
|
419
|
+
const requestBody = new URLSearchParams({
|
|
420
|
+
client_id: this.oauthClientId,
|
|
421
|
+
client_secret: this.oauthClientSecret,
|
|
422
|
+
code: redirectUrl.searchParams.get("code"),
|
|
423
|
+
grant_type: "authorization_code",
|
|
424
|
+
redirect_uri: this.oauthRedirectUri,
|
|
425
|
+
scope: redirectUrl.searchParams.get("scope"),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Now we execute the final login redirect that will
|
|
429
|
+
// return our access and refresh tokens.
|
|
430
|
+
response = await this.fetch(NUHEAT_API_TOKEN_URI, {
|
|
431
|
+
body: requestBody.toString(),
|
|
432
|
+
headers: {
|
|
433
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
434
|
+
},
|
|
435
|
+
method: "POST",
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (!response) {
|
|
439
|
+
this.log.error("NuHeatAPI: Unable to acquire an OAuth access token.");
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
// Grab the token JSON.
|
|
443
|
+
this.tokenScope = redirectUrl.searchParams.get("scope") ?? "";
|
|
444
|
+
|
|
445
|
+
// Return the access token
|
|
446
|
+
return await response.json();
|
|
447
|
+
} else {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Refresh our OAuth access token.
|
|
453
|
+
async getRefreshedAccessToken() {
|
|
454
|
+
// Create the request to refresh tokens.
|
|
455
|
+
const requestBody = new URLSearchParams({
|
|
456
|
+
client_id: this.oauthClientId,
|
|
457
|
+
client_secret: this.oauthClientSecret,
|
|
458
|
+
grant_type: "refresh_token",
|
|
459
|
+
redirect_uri: this.oauthRedirectUri,
|
|
460
|
+
refresh_token: this.refreshToken,
|
|
461
|
+
scope: this.tokenScope,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Execute the refresh token request.
|
|
465
|
+
const response = await this.fetch(NUHEAT_API_TOKEN_URI, {
|
|
466
|
+
body: requestBody.toString(),
|
|
467
|
+
headers: {
|
|
468
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
469
|
+
},
|
|
470
|
+
method: "POST",
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (!response) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Grab the refresh token JSON.
|
|
478
|
+
const token = await response.json();
|
|
479
|
+
this.accessToken = token.access_token;
|
|
480
|
+
this.accessTokenTimestamp = Date.now();
|
|
481
|
+
this.refreshInterval = token.expires_in;
|
|
482
|
+
this.refreshToken = token.refresh_token;
|
|
483
|
+
this.tokenScope = token.scope ?? this.tokenScope;
|
|
484
|
+
this.tokenType = token.token_type;
|
|
485
|
+
|
|
486
|
+
// Refresh our tokens at seven minutes before expiration as a failsafe.
|
|
487
|
+
this.refreshInterval -= 420;
|
|
488
|
+
|
|
489
|
+
// Ensure we never try to refresh more frequently than every five minutes.
|
|
490
|
+
if (this.refreshInterval < 300) {
|
|
491
|
+
this.refreshInterval = 300;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Update our authorization header.
|
|
495
|
+
this.headers.set(
|
|
496
|
+
"Authorization",
|
|
497
|
+
token.token_type + " " + token.access_token,
|
|
498
|
+
);
|
|
499
|
+
this.log.debug(
|
|
500
|
+
"NuHeatAPI: Successfully refreshed the NuHeat API access token.",
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// We're done.
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Return access token if we have one. If not, get one now
|
|
508
|
+
async returnAccessToken() {
|
|
509
|
+
if (this.accessToken) {
|
|
510
|
+
if (
|
|
511
|
+
Date.now() - this.accessTokenTimestamp > this.refreshInterval * 1000 &&
|
|
512
|
+
Date.now() - this.accessTokenTimestamp < 1000 * 60 * 60 * 24 * 13.5
|
|
513
|
+
) {
|
|
514
|
+
await this.getRefreshedAccessToken();
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
await this.acquireAccessToken();
|
|
518
|
+
}
|
|
519
|
+
if (this.accessToken) {
|
|
520
|
+
this.headers.set(
|
|
521
|
+
"Authorization",
|
|
522
|
+
this.tokenType + " " + this.accessToken,
|
|
523
|
+
);
|
|
524
|
+
return this.accessToken;
|
|
525
|
+
} else {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Log us into NuHeat and get an access token.
|
|
531
|
+
async acquireAccessToken() {
|
|
532
|
+
let firstConnection = true;
|
|
533
|
+
|
|
534
|
+
// Clear out tokens from prior connections.
|
|
535
|
+
if (this.accessToken) {
|
|
536
|
+
firstConnection = false;
|
|
537
|
+
this.accessToken = null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Login to the NuHeat API and get an OAuth access token for our session.
|
|
541
|
+
const token = await this.getAccessToken();
|
|
542
|
+
|
|
543
|
+
if (!token) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// On initial plugin startup, let the user know we've successfully connected.
|
|
548
|
+
if (firstConnection) {
|
|
549
|
+
this.log.info("NuHeatAPI: Successfully connected to the NuHeat API.");
|
|
550
|
+
} else {
|
|
551
|
+
this.log.debug(
|
|
552
|
+
"NuHeatAPI: Successfully reacquired a NuHeat API access token.",
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
this.accessToken = token.access_token;
|
|
557
|
+
this.accessTokenTimestamp = Date.now();
|
|
558
|
+
this.tokenType = token.token_type;
|
|
559
|
+
this.refreshInterval = token.expires_in;
|
|
560
|
+
this.refreshToken = token.refresh_token;
|
|
561
|
+
this.tokenScope = token.scope ?? this.tokenScope;
|
|
562
|
+
|
|
563
|
+
// Add the token to our headers that we will use for subsequent API calls.
|
|
564
|
+
this.headers.set("Authorization", this.tokenType + " " + this.accessToken);
|
|
565
|
+
|
|
566
|
+
// Success.
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Refresh the NuHeat API access token, if needed.
|
|
571
|
+
async refreshAccessToken() {
|
|
572
|
+
// If we don't have a access token yet, acquire one.
|
|
573
|
+
if (!this.accessToken) {
|
|
574
|
+
this.log.debug(
|
|
575
|
+
"NuHeatAPI: Acquiring new access token. Ours seems to be missing",
|
|
576
|
+
);
|
|
577
|
+
return await this.acquireAccessToken();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Is it time to refresh? If not, we're good for now.
|
|
581
|
+
if (Date.now() - this.accessTokenTimestamp < this.refreshInterval * 1000) {
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
this.log.debug(
|
|
585
|
+
"NuHeatAPI: Acquiring new access token. Ours has expired or is expiring soon",
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
// Try refreshing our existing access token before resorting to acquiring a new one.
|
|
589
|
+
if (await this.getRefreshedAccessToken()) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this.log.error(
|
|
594
|
+
"NuHeatAPI: Unable to refresh our access token. " +
|
|
595
|
+
"This error can usually be safely ignored and will be resolved by acquiring a new access token.",
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// Now generate a new access token.
|
|
599
|
+
if (!(await this.acquireAccessToken())) {
|
|
600
|
+
this.log.error(
|
|
601
|
+
"NuHeatAPI: Fatal error. We need a new access token didnt successfuly get one",
|
|
602
|
+
);
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Utility to let us streamline error handling and return checking from the NuHeat API.
|
|
609
|
+
async fetch(url, options = {}, decodeResponse = true, isRetry = false) {
|
|
610
|
+
// Set our headers.
|
|
611
|
+
if (!options.headers) {
|
|
612
|
+
options.headers = this.headers;
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
let response = await fetch(url, options);
|
|
616
|
+
// The caller will sort through responses instead of us.
|
|
617
|
+
if (!decodeResponse) {
|
|
618
|
+
return response;
|
|
619
|
+
}
|
|
620
|
+
// Bad form data submitted.
|
|
621
|
+
if (response.status === 400) {
|
|
622
|
+
this.log.error(
|
|
623
|
+
"NuHeatAPI: Invalid call. Data submitted doesn't seem right",
|
|
624
|
+
);
|
|
625
|
+
return null;
|
|
626
|
+
// Bad username and password.
|
|
627
|
+
} else if (response.status === 401) {
|
|
628
|
+
this.log.error(
|
|
629
|
+
"NuHeatAPI: Invalid NuHeat credentials given. Check your login and password.",
|
|
630
|
+
);
|
|
631
|
+
return null;
|
|
632
|
+
// Error on the NuHeat side.
|
|
633
|
+
} else if (response.status === 500) {
|
|
634
|
+
this.log.error("NuHeatAPI: NuHeat had an internal server error.");
|
|
635
|
+
if (isRetry) {
|
|
636
|
+
return null;
|
|
637
|
+
} else {
|
|
638
|
+
this.log.error("NuHeatAPI: Trying again.");
|
|
639
|
+
return this.fetch(url, options, decodeResponse, true);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Some other unknown error occurred.
|
|
643
|
+
if (!response.ok && !isRedirect(response.status)) {
|
|
644
|
+
this.log.error(
|
|
645
|
+
"NuHeatAPI: " +
|
|
646
|
+
url +
|
|
647
|
+
" Error: " +
|
|
648
|
+
response.status +
|
|
649
|
+
" " +
|
|
650
|
+
response.statusText,
|
|
651
|
+
);
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
return response;
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if (error instanceof FetchError) {
|
|
657
|
+
switch (error.code) {
|
|
658
|
+
case "ECONNREFUSED":
|
|
659
|
+
this.log.error("NuHeatAPI: Connection refused.");
|
|
660
|
+
break;
|
|
661
|
+
case "ECONNRESET":
|
|
662
|
+
// Retry on connection reset, but no more than once.
|
|
663
|
+
if (!isRetry) {
|
|
664
|
+
this.log.debug(
|
|
665
|
+
"NuHeatAPI: Connection has been reset. Retrying the API action.",
|
|
666
|
+
);
|
|
667
|
+
return this.fetch(url, options, decodeResponse, true);
|
|
668
|
+
}
|
|
669
|
+
this.log.error("NuHeatAPI: Connection has been reset.");
|
|
670
|
+
break;
|
|
671
|
+
case "ENOTFOUND":
|
|
672
|
+
this.log.error("NuHeatAPI: Hostname or IP address not found.");
|
|
673
|
+
break;
|
|
674
|
+
case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
|
|
675
|
+
this.log.error(
|
|
676
|
+
"NuHeatAPI: Unable to verify the NuHeat TLS security certificate.",
|
|
677
|
+
);
|
|
678
|
+
break;
|
|
679
|
+
default:
|
|
680
|
+
this.log.error(error.message);
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
this.log.error("Unknown fetch error: " + error);
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
trimSetCookie(setCookie) {
|
|
690
|
+
if (setCookie) {
|
|
691
|
+
return setCookie.map((x) => x.split(";")[0]).join("; ");
|
|
692
|
+
} else {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
};
|