homebridge-nuheat2 1.2.14 → 1.2.16
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 +13 -0
- package/README.md +7 -5
- package/config.schema.json +3 -3
- package/homebridge-ui/public/index.html +9 -7
- package/homebridge-ui/public/index.js +7 -16
- package/homebridge-ui/public/styles.css +68 -9
- package/lib/NuHeatAPI.js +64 -34
- package/lib/settings.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this project should be documented in this file
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.2.16] - 2026-04-29
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Switch the built-in Nuheat OAuth client to Authorization Code with PKCE using the public `homebridge-nuheat2_260421` client ID
|
|
12
|
+
- Keep legacy confidential-client OAuth overrides available when a custom `clientSecret` is explicitly configured
|
|
13
|
+
|
|
14
|
+
## [1.2.15] - 2026-04-27
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Restore Homebridge-like typography, form controls, and button styling in the custom admin UI while keeping theme-compatible colors
|
|
19
|
+
|
|
7
20
|
## [1.2.14] - 2026-04-27
|
|
8
21
|
|
|
9
22
|
### Changed
|
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ This project builds on the original [`senorshaun/homebridge-nuheat`](https://git
|
|
|
15
15
|
- Optionally creates HomeKit switches for Nuheat group away mode
|
|
16
16
|
- Supports permanent, scheduled, and timed holds
|
|
17
17
|
- Uses Nuheat's OAuth-based API instead of legacy site scraping
|
|
18
|
+
- Uses the official Nuheat PKCE public client by default, with no distributable client secret
|
|
18
19
|
- Includes compatibility improvements for Homebridge 1.8+ and 2.0 betas
|
|
19
20
|
- Can optionally expose a schedule switch for each thermostat
|
|
20
21
|
- Allows advanced OAuth overrides for long-term API stability
|
|
@@ -89,8 +90,8 @@ The equivalent JSON looks like this:
|
|
|
89
90
|
- `refresh`: Poll interval in seconds, default `60`. Values lower than `30` are raised to `30` to reduce API traffic
|
|
90
91
|
- `enableNotifications`: Enables Nuheat SignalR notifications for faster updates. Defaults to `true`; set to `false` only while troubleshooting
|
|
91
92
|
- `debug`: Enables verbose Nuheat API, notification, and accessory logging. Defaults to `false`
|
|
92
|
-
- `clientId`: Optional advanced override for the Nuheat OAuth client ID.
|
|
93
|
-
- `clientSecret`: Optional legacy OAuth client secret override for confidential-client credentials.
|
|
93
|
+
- `clientId`: Optional advanced override for the Nuheat OAuth client ID. Leave blank to use the built-in PKCE public client ID, `homebridge-nuheat2_260421`
|
|
94
|
+
- `clientSecret`: Optional legacy OAuth client secret override for confidential-client credentials. Leave blank for the PKCE public-client flow. Do not publish or share this value
|
|
94
95
|
- `redirectUri`: Optional advanced override for the Nuheat OAuth redirect URI, default `http://localhost`
|
|
95
96
|
|
|
96
97
|
### Hold Length Behavior
|
|
@@ -114,7 +115,9 @@ Nuheat's public OpenAPI documentation indicates that third-party developers shou
|
|
|
114
115
|
- [Nuheat OpenAPI docs](https://api.mynuheat.com/)
|
|
115
116
|
- [Nuheat API access request page](https://www.nuheat.com/openapi)
|
|
116
117
|
|
|
117
|
-
This fork
|
|
118
|
+
This fork uses Nuheat's PKCE-based public client for normal authentication. The built-in public `clientId` is `homebridge-nuheat2_260421`; there is no distributable client secret.
|
|
119
|
+
|
|
120
|
+
Legacy confidential-client credentials can still be supplied with `clientId` and `clientSecret` for testing alternate Nuheat-issued clients. Keep any `clientSecret` out of GitHub, npm, screenshots, and shared logs.
|
|
118
121
|
|
|
119
122
|
## What's New In This Fork
|
|
120
123
|
|
|
@@ -173,7 +176,6 @@ After that, bump the version in `package.json`, push to `master`, and GitHub Act
|
|
|
173
176
|
|
|
174
177
|
## Future Work
|
|
175
178
|
|
|
176
|
-
- Validate the
|
|
177
|
-
- Move the normal OAuth path to Nuheat's PKCE-based public client once the new client details are issued.
|
|
179
|
+
- Validate the PKCE public-client flow against more real-world Nuheat accounts.
|
|
178
180
|
- Verify group and away-mode behavior against current live API responses.
|
|
179
181
|
- Evaluate whether SignalR notifications can reduce polling further in real-world deployments.
|
package/config.schema.json
CHANGED
|
@@ -125,13 +125,13 @@
|
|
|
125
125
|
"clientId": {
|
|
126
126
|
"title": "Nuheat Client ID",
|
|
127
127
|
"type": "string",
|
|
128
|
-
"description": "Advanced OAuth override.
|
|
128
|
+
"description": "Advanced OAuth override. Leave blank to use the built-in PKCE public client ID."
|
|
129
129
|
},
|
|
130
130
|
"clientSecret": {
|
|
131
131
|
"title": "Nuheat Client Secret (Legacy)",
|
|
132
132
|
"type": "string",
|
|
133
133
|
"format": "password",
|
|
134
|
-
"description": "
|
|
134
|
+
"description": "Legacy OAuth override for confidential-client credentials. Leave blank for the PKCE public-client flow. Do not publish or share this value."
|
|
135
135
|
},
|
|
136
136
|
"redirectUri": {
|
|
137
137
|
"title": "Nuheat Redirect URI",
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
"title": "Advanced OAuth",
|
|
178
178
|
"expandable": true,
|
|
179
179
|
"expanded": false,
|
|
180
|
-
"description": "Only change these fields when testing Nuheat-issued API credentials.",
|
|
180
|
+
"description": "Only change these fields when testing alternate Nuheat-issued API credentials.",
|
|
181
181
|
"items": [
|
|
182
182
|
"clientId",
|
|
183
183
|
"clientSecret",
|
|
@@ -173,19 +173,21 @@
|
|
|
173
173
|
<div>
|
|
174
174
|
<h2>Advanced OAuth</h2>
|
|
175
175
|
<p class="help">
|
|
176
|
-
Leave these fields blank
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
Leave these fields blank to use the built-in Nuheat PKCE public
|
|
177
|
+
client. Only change them when testing alternate Nuheat-issued API
|
|
178
|
+
credentials.
|
|
179
179
|
</p>
|
|
180
180
|
</div>
|
|
181
|
-
<span id="oauth-status" class="status-pill
|
|
181
|
+
<span id="oauth-status" class="status-pill good">Built-in PKCE</span>
|
|
182
182
|
</div>
|
|
183
183
|
|
|
184
184
|
<div class="settings-grid">
|
|
185
185
|
<label class="field">
|
|
186
186
|
<span>Nuheat Client ID</span>
|
|
187
187
|
<input id="client-id" type="text" placeholder="Optional client ID" />
|
|
188
|
-
<small>
|
|
188
|
+
<small>
|
|
189
|
+
Advanced override. Blank uses homebridge-nuheat2_260421.
|
|
190
|
+
</small>
|
|
189
191
|
</label>
|
|
190
192
|
|
|
191
193
|
<label id="client-secret-row" class="field">
|
|
@@ -196,8 +198,8 @@
|
|
|
196
198
|
placeholder="Leave blank to keep existing"
|
|
197
199
|
/>
|
|
198
200
|
<small>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
Leave blank for PKCE public-client auth. Only use this for legacy
|
|
202
|
+
confidential-client credentials.
|
|
201
203
|
</small>
|
|
202
204
|
</label>
|
|
203
205
|
|
|
@@ -320,15 +320,6 @@ async function saveOauth() {
|
|
|
320
320
|
return;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
if (clientId && !hasUsableClientSecret(clientId) && !clientSecret) {
|
|
324
|
-
showToast(
|
|
325
|
-
"error",
|
|
326
|
-
"Current OAuth overrides require a matching client ID and client secret.",
|
|
327
|
-
);
|
|
328
|
-
elements.clientSecret.focus();
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
323
|
const patch = {
|
|
333
324
|
clientId: clientId || undefined,
|
|
334
325
|
redirectUri: redirectUri && redirectUri !== "http://localhost" ? redirectUri : undefined,
|
|
@@ -336,7 +327,7 @@ async function saveOauth() {
|
|
|
336
327
|
|
|
337
328
|
if (clientSecret) {
|
|
338
329
|
patch.clientSecret = clientSecret;
|
|
339
|
-
} else if (!clientId) {
|
|
330
|
+
} else if (!clientId || clientId !== (state.config?.clientId || "")) {
|
|
340
331
|
patch.clientSecret = undefined;
|
|
341
332
|
}
|
|
342
333
|
|
|
@@ -450,10 +441,10 @@ function updateOauthStatus() {
|
|
|
450
441
|
const hasClientSecret = hasUsableClientSecret(elements.clientId.value.trim());
|
|
451
442
|
const text = hasClientId
|
|
452
443
|
? hasClientSecret
|
|
453
|
-
? "Custom OAuth"
|
|
454
|
-
: "
|
|
455
|
-
: "Built-in
|
|
456
|
-
setStatus(elements.oauthStatus,
|
|
444
|
+
? "Custom legacy OAuth"
|
|
445
|
+
: "Custom PKCE"
|
|
446
|
+
: "Built-in PKCE";
|
|
447
|
+
setStatus(elements.oauthStatus, true, text);
|
|
457
448
|
}
|
|
458
449
|
|
|
459
450
|
function hasUsableClientSecret(clientId) {
|
|
@@ -590,9 +581,9 @@ function getOauthMode(config) {
|
|
|
590
581
|
return "configured confidential client";
|
|
591
582
|
}
|
|
592
583
|
if (config.clientId) {
|
|
593
|
-
return "
|
|
584
|
+
return "configured PKCE public client";
|
|
594
585
|
}
|
|
595
|
-
return "built-in
|
|
586
|
+
return "built-in PKCE public client";
|
|
596
587
|
}
|
|
597
588
|
|
|
598
589
|
function normalizeHoldLength(value) {
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
--plugin-
|
|
3
|
-
--bs-
|
|
4
|
-
|
|
2
|
+
--plugin-font-family: var(
|
|
3
|
+
--bs-body-font-family,
|
|
4
|
+
-apple-system,
|
|
5
|
+
BlinkMacSystemFont,
|
|
6
|
+
"Segoe UI",
|
|
7
|
+
Roboto,
|
|
8
|
+
"Helvetica Neue",
|
|
9
|
+
Arial,
|
|
10
|
+
sans-serif
|
|
5
11
|
);
|
|
6
|
-
--plugin-
|
|
7
|
-
--plugin-
|
|
12
|
+
--plugin-border: color-mix(in srgb, currentColor 24%, transparent);
|
|
13
|
+
--plugin-strong-border: color-mix(in srgb, currentColor 36%, transparent);
|
|
14
|
+
--plugin-subtle-surface: color-mix(in srgb, currentColor 6%, transparent);
|
|
15
|
+
--plugin-control-surface: color-mix(in srgb, currentColor 7%, transparent);
|
|
16
|
+
--plugin-control-disabled: color-mix(in srgb, currentColor 4%, transparent);
|
|
8
17
|
--plugin-accent: var(--bs-primary, #0a84ff);
|
|
9
18
|
--plugin-accent-surface: rgba(10, 132, 255, 0.08);
|
|
10
19
|
--plugin-warning-surface: rgba(245, 158, 11, 0.12);
|
|
@@ -22,7 +31,9 @@ body {
|
|
|
22
31
|
margin: 0;
|
|
23
32
|
color: inherit;
|
|
24
33
|
background: transparent;
|
|
25
|
-
font:
|
|
34
|
+
font-family: var(--plugin-font-family);
|
|
35
|
+
font-size: 1rem;
|
|
36
|
+
line-height: 1.5;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
.container {
|
|
@@ -37,6 +48,7 @@ body {
|
|
|
37
48
|
header h1 {
|
|
38
49
|
margin: 0 0 8px;
|
|
39
50
|
font-size: 2rem;
|
|
51
|
+
font-weight: 600;
|
|
40
52
|
line-height: 1.1;
|
|
41
53
|
}
|
|
42
54
|
|
|
@@ -64,12 +76,14 @@ header h1 {
|
|
|
64
76
|
.panel h2 {
|
|
65
77
|
margin: 0 0 6px;
|
|
66
78
|
font-size: 1.18rem;
|
|
79
|
+
font-weight: 600;
|
|
67
80
|
line-height: 1.25;
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
.panel h3 {
|
|
71
84
|
margin: 0 0 4px;
|
|
72
85
|
font-size: 1rem;
|
|
86
|
+
font-weight: 600;
|
|
73
87
|
line-height: 1.25;
|
|
74
88
|
}
|
|
75
89
|
|
|
@@ -104,6 +118,32 @@ header h1 {
|
|
|
104
118
|
width: 100%;
|
|
105
119
|
}
|
|
106
120
|
|
|
121
|
+
input,
|
|
122
|
+
select {
|
|
123
|
+
display: block;
|
|
124
|
+
width: 100%;
|
|
125
|
+
min-height: 38px;
|
|
126
|
+
padding: 0.45rem 0.75rem;
|
|
127
|
+
color: inherit;
|
|
128
|
+
background: var(--bs-body-bg, var(--plugin-control-surface));
|
|
129
|
+
border: 1px solid var(--plugin-border);
|
|
130
|
+
border-radius: 0.375rem;
|
|
131
|
+
font-family: var(--plugin-font-family);
|
|
132
|
+
font-size: 1rem;
|
|
133
|
+
line-height: 1.5;
|
|
134
|
+
transition:
|
|
135
|
+
border-color 0.15s ease,
|
|
136
|
+
box-shadow 0.15s ease,
|
|
137
|
+
background-color 0.15s ease;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
input:focus,
|
|
141
|
+
select:focus {
|
|
142
|
+
border-color: color-mix(in srgb, var(--plugin-accent) 72%, currentColor);
|
|
143
|
+
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--plugin-accent) 18%, transparent);
|
|
144
|
+
outline: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
.field small,
|
|
108
148
|
.checkbox small {
|
|
109
149
|
color: currentColor;
|
|
@@ -139,6 +179,7 @@ input::placeholder {
|
|
|
139
179
|
}
|
|
140
180
|
|
|
141
181
|
.readonly input {
|
|
182
|
+
background: var(--plugin-control-disabled);
|
|
142
183
|
opacity: 0.68;
|
|
143
184
|
}
|
|
144
185
|
|
|
@@ -150,7 +191,22 @@ input::placeholder {
|
|
|
150
191
|
}
|
|
151
192
|
|
|
152
193
|
button {
|
|
153
|
-
|
|
194
|
+
display: inline-flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
justify-content: center;
|
|
197
|
+
min-height: 38px;
|
|
198
|
+
padding: 0.45rem 0.85rem;
|
|
199
|
+
border: 1px solid transparent;
|
|
200
|
+
font-family: var(--plugin-font-family);
|
|
201
|
+
font-size: 0.95rem;
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
line-height: 1.5;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
transition:
|
|
206
|
+
background-color 0.15s ease,
|
|
207
|
+
border-color 0.15s ease,
|
|
208
|
+
box-shadow 0.15s ease,
|
|
209
|
+
filter 0.15s ease;
|
|
154
210
|
}
|
|
155
211
|
|
|
156
212
|
button.primary,
|
|
@@ -166,13 +222,14 @@ button.primary {
|
|
|
166
222
|
|
|
167
223
|
button.secondary {
|
|
168
224
|
color: inherit;
|
|
169
|
-
background:
|
|
225
|
+
background: var(--plugin-subtle-surface);
|
|
170
226
|
border: 1px solid var(--plugin-border);
|
|
171
227
|
}
|
|
172
228
|
|
|
173
229
|
button.primary:hover,
|
|
174
230
|
button.secondary:hover {
|
|
175
231
|
filter: brightness(1.04);
|
|
232
|
+
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--plugin-accent) 10%, transparent);
|
|
176
233
|
}
|
|
177
234
|
|
|
178
235
|
.toast-container {
|
|
@@ -187,7 +244,7 @@ button.secondary:hover {
|
|
|
187
244
|
|
|
188
245
|
.toast {
|
|
189
246
|
color: inherit;
|
|
190
|
-
background: var(--bs-body-bg, var(--plugin-
|
|
247
|
+
background: var(--bs-body-bg, var(--plugin-control-surface));
|
|
191
248
|
border: 1px solid var(--plugin-border);
|
|
192
249
|
border-radius: 8px;
|
|
193
250
|
padding: 12px 16px;
|
|
@@ -310,12 +367,14 @@ button.secondary:hover {
|
|
|
310
367
|
.status-pill.good {
|
|
311
368
|
background: var(--plugin-success-surface);
|
|
312
369
|
border-color: rgba(34, 197, 94, 0.42);
|
|
370
|
+
color: inherit;
|
|
313
371
|
}
|
|
314
372
|
|
|
315
373
|
.pill.warn,
|
|
316
374
|
.status-pill.warn {
|
|
317
375
|
background: var(--plugin-warning-surface);
|
|
318
376
|
border-color: rgba(245, 158, 11, 0.42);
|
|
377
|
+
color: inherit;
|
|
319
378
|
}
|
|
320
379
|
|
|
321
380
|
@keyframes slide-in {
|
package/lib/NuHeatAPI.js
CHANGED
|
@@ -6,6 +6,7 @@ const FetchError = fetchModule.FetchError;
|
|
|
6
6
|
const HeadersCtor = fetchModule.Headers;
|
|
7
7
|
const isRedirect = fetchModule.isRedirect;
|
|
8
8
|
const { parse } = htmlParser;
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
10
|
const settings_1 = require("./settings");
|
|
10
11
|
const NuHeatModels_1 = require("./NuHeatModels");
|
|
11
12
|
const OAUTH_SCOPES = ["openapi", "openid", "profile", "offline_access"];
|
|
@@ -16,7 +17,9 @@ class NuHeatAPI {
|
|
|
16
17
|
oauthClientId;
|
|
17
18
|
oauthClientSecret;
|
|
18
19
|
oauthRedirectUri;
|
|
19
|
-
|
|
20
|
+
usingBuiltInClient;
|
|
21
|
+
usePkce;
|
|
22
|
+
pkceCodeVerifier;
|
|
20
23
|
headers;
|
|
21
24
|
accessToken;
|
|
22
25
|
accessTokenTimestamp;
|
|
@@ -28,23 +31,19 @@ class NuHeatAPI {
|
|
|
28
31
|
this.email = email;
|
|
29
32
|
this.password = password;
|
|
30
33
|
this.log = log;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
process.env.NUHEAT_API_CLIENT_SECRET ||
|
|
38
|
-
settings_1.NUHEAT_API_CLIENT_SECRET;
|
|
34
|
+
const configuredClientId = options.clientId || process.env.NUHEAT_API_CLIENT_ID || "";
|
|
35
|
+
const configuredClientSecret = options.clientSecret || process.env.NUHEAT_API_CLIENT_SECRET || "";
|
|
36
|
+
this.oauthClientId = configuredClientId || settings_1.NUHEAT_API_CLIENT_ID;
|
|
37
|
+
this.oauthClientSecret = configuredClientId
|
|
38
|
+
? configuredClientSecret || settings_1.NUHEAT_API_CLIENT_SECRET
|
|
39
|
+
: "";
|
|
39
40
|
this.oauthRedirectUri =
|
|
40
41
|
options.redirectUri ||
|
|
41
42
|
process.env.NUHEAT_API_REDIRECT_URI ||
|
|
42
43
|
settings_1.NUHEAT_API_REDIRECT_URI;
|
|
43
|
-
this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
!options.clientSecret &&
|
|
47
|
-
!process.env.NUHEAT_API_CLIENT_SECRET;
|
|
44
|
+
this.usingBuiltInClient = !configuredClientId;
|
|
45
|
+
this.usePkce = !this.oauthClientSecret;
|
|
46
|
+
this.pkceCodeVerifier = "";
|
|
48
47
|
this.headers = new HeadersCtor();
|
|
49
48
|
this.headers.set("Content-Type", "application/json");
|
|
50
49
|
this.headers.set("Accept", "application/json");
|
|
@@ -54,8 +53,10 @@ class NuHeatAPI {
|
|
|
54
53
|
this.refreshToken = "";
|
|
55
54
|
this.tokenScope = "";
|
|
56
55
|
this.tokenType = "Bearer";
|
|
57
|
-
if (this.
|
|
58
|
-
this.log.
|
|
56
|
+
if (this.usingBuiltInClient) {
|
|
57
|
+
this.log.info("NuHeatAPI: Using built-in Nuheat PKCE public client ID " +
|
|
58
|
+
this.oauthClientId +
|
|
59
|
+
".");
|
|
59
60
|
}
|
|
60
61
|
else {
|
|
61
62
|
this.log.info("NuHeatAPI: Using configured OAuth client ID " + this.oauthClientId + ".");
|
|
@@ -63,11 +64,48 @@ class NuHeatAPI {
|
|
|
63
64
|
this.log.debug("NuHeatAPI: OAuth redirect URI " +
|
|
64
65
|
this.oauthRedirectUri +
|
|
65
66
|
". Requested scopes: " +
|
|
66
|
-
this.getRequestedScope()
|
|
67
|
+
this.getRequestedScope() +
|
|
68
|
+
". OAuth flow: " +
|
|
69
|
+
(this.usePkce ? "authorization_code_pkce" : "authorization_code_secret"));
|
|
67
70
|
}
|
|
68
71
|
getRequestedScope() {
|
|
69
72
|
return OAUTH_SCOPES.join(" ");
|
|
70
73
|
}
|
|
74
|
+
generatePkceCodeVerifier() {
|
|
75
|
+
return (0, node_crypto_1.randomBytes)(64).toString("base64url");
|
|
76
|
+
}
|
|
77
|
+
getPkceCodeChallenge(codeVerifier) {
|
|
78
|
+
return (0, node_crypto_1.createHash)("sha256").update(codeVerifier).digest("base64url");
|
|
79
|
+
}
|
|
80
|
+
buildAuthorizationCodeTokenRequest(redirectUrl) {
|
|
81
|
+
const requestBody = new URLSearchParams({
|
|
82
|
+
client_id: this.oauthClientId,
|
|
83
|
+
code: redirectUrl.searchParams.get("code") || "",
|
|
84
|
+
grant_type: "authorization_code",
|
|
85
|
+
redirect_uri: this.oauthRedirectUri,
|
|
86
|
+
scope: redirectUrl.searchParams.get("scope") || "",
|
|
87
|
+
});
|
|
88
|
+
if (this.usePkce) {
|
|
89
|
+
requestBody.set("code_verifier", this.pkceCodeVerifier);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
requestBody.set("client_secret", this.oauthClientSecret);
|
|
93
|
+
}
|
|
94
|
+
return requestBody;
|
|
95
|
+
}
|
|
96
|
+
buildRefreshTokenRequest() {
|
|
97
|
+
const requestBody = new URLSearchParams({
|
|
98
|
+
client_id: this.oauthClientId,
|
|
99
|
+
grant_type: "refresh_token",
|
|
100
|
+
refresh_token: this.refreshToken,
|
|
101
|
+
scope: this.tokenScope,
|
|
102
|
+
});
|
|
103
|
+
if (!this.usePkce) {
|
|
104
|
+
requestBody.set("client_secret", this.oauthClientSecret);
|
|
105
|
+
requestBody.set("redirect_uri", this.oauthRedirectUri);
|
|
106
|
+
}
|
|
107
|
+
return requestBody;
|
|
108
|
+
}
|
|
71
109
|
async setAwayMode(groupId, awayMode) {
|
|
72
110
|
const callURL = "https://api.mynuheat.com/api/v1/Group";
|
|
73
111
|
const callOptions = {
|
|
@@ -248,8 +286,14 @@ class NuHeatAPI {
|
|
|
248
286
|
authEndpoint.searchParams.set("client_id", this.oauthClientId);
|
|
249
287
|
authEndpoint.searchParams.set("redirect_uri", this.oauthRedirectUri);
|
|
250
288
|
authEndpoint.searchParams.set("scope", this.getRequestedScope());
|
|
289
|
+
if (this.usePkce) {
|
|
290
|
+
this.pkceCodeVerifier = this.generatePkceCodeVerifier();
|
|
291
|
+
authEndpoint.searchParams.set("code_challenge", this.getPkceCodeChallenge(this.pkceCodeVerifier));
|
|
292
|
+
authEndpoint.searchParams.set("code_challenge_method", "S256");
|
|
293
|
+
}
|
|
251
294
|
this.log.debug("NuHeatAPI: Requesting OAuth authorization page with scopes: " +
|
|
252
|
-
this.getRequestedScope()
|
|
295
|
+
this.getRequestedScope() +
|
|
296
|
+
(this.usePkce ? " using PKCE." : "."));
|
|
253
297
|
const response = await this.fetch(authEndpoint.toString(), {
|
|
254
298
|
redirect: "follow",
|
|
255
299
|
});
|
|
@@ -396,14 +440,7 @@ class NuHeatAPI {
|
|
|
396
440
|
return null;
|
|
397
441
|
}
|
|
398
442
|
const redirectUrl = new URL(response.headers.get("location") || "");
|
|
399
|
-
const requestBody =
|
|
400
|
-
client_id: this.oauthClientId,
|
|
401
|
-
client_secret: this.oauthClientSecret,
|
|
402
|
-
code: redirectUrl.searchParams.get("code") || "",
|
|
403
|
-
grant_type: "authorization_code",
|
|
404
|
-
redirect_uri: this.oauthRedirectUri,
|
|
405
|
-
scope: redirectUrl.searchParams.get("scope") || "",
|
|
406
|
-
});
|
|
443
|
+
const requestBody = this.buildAuthorizationCodeTokenRequest(redirectUrl);
|
|
407
444
|
response = await this.fetch(settings_1.NUHEAT_API_TOKEN_URI, {
|
|
408
445
|
body: requestBody.toString(),
|
|
409
446
|
headers: {
|
|
@@ -426,14 +463,7 @@ class NuHeatAPI {
|
|
|
426
463
|
return null;
|
|
427
464
|
}
|
|
428
465
|
async getRefreshedAccessToken() {
|
|
429
|
-
const requestBody =
|
|
430
|
-
client_id: this.oauthClientId,
|
|
431
|
-
client_secret: this.oauthClientSecret,
|
|
432
|
-
grant_type: "refresh_token",
|
|
433
|
-
redirect_uri: this.oauthRedirectUri,
|
|
434
|
-
refresh_token: this.refreshToken,
|
|
435
|
-
scope: this.tokenScope,
|
|
436
|
-
});
|
|
466
|
+
const requestBody = this.buildRefreshTokenRequest();
|
|
437
467
|
const response = await this.fetch(settings_1.NUHEAT_API_TOKEN_URI, {
|
|
438
468
|
body: requestBody.toString(),
|
|
439
469
|
headers: {
|
package/lib/settings.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.NUHEAT_API_CONSENT_URI = exports.NUHEAT_API_TOKEN_URI = exports.NUHEAT_API_AUTHORIZE_URI = exports.NUHEAT_API_REDIRECT_URI = exports.NUHEAT_API_CLIENT_SECRET = exports.NUHEAT_API_CLIENT_ID = void 0;
|
|
4
|
-
exports.NUHEAT_API_CLIENT_ID = "homebridge-
|
|
5
|
-
exports.NUHEAT_API_CLIENT_SECRET = "
|
|
4
|
+
exports.NUHEAT_API_CLIENT_ID = "homebridge-nuheat2_260421";
|
|
5
|
+
exports.NUHEAT_API_CLIENT_SECRET = "";
|
|
6
6
|
exports.NUHEAT_API_REDIRECT_URI = "http://localhost";
|
|
7
7
|
exports.NUHEAT_API_AUTHORIZE_URI = "https://identity.mynuheat.com/connect/authorize";
|
|
8
8
|
exports.NUHEAT_API_TOKEN_URI = "https://identity.mynuheat.com/connect/token";
|