homebridge-nuheat2 1.2.17 → 1.2.19

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 CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project should be documented in this file
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.19] - 2026-04-30
8
+
9
+ ### Fixed
10
+
11
+ - Make saved thermostat and group allow-list values readable and easier to edit in the custom admin UI
12
+
13
+ ## [1.2.18] - 2026-04-29
14
+
15
+ ### Changed
16
+
17
+ - Remove user-facing Advanced OAuth settings from the Homebridge schema, custom UI, and README now that Nuheat provides a built-in PKCE public client for this plugin
18
+ - Remove the editable Platform Name field from the custom admin UI while keeping the config key supported for backward compatibility
19
+ - Improve Accessories allow-list rendering so saved thermostat and group rows are normalized and displayed as clearly editable rows
20
+
7
21
  ## [1.2.17] - 2026-04-29
8
22
 
9
23
  ### Fixed
package/README.md CHANGED
@@ -18,7 +18,6 @@ This project builds on the original [`senorshaun/homebridge-nuheat`](https://git
18
18
  - Uses the official Nuheat PKCE public client by default, with no distributable client secret
19
19
  - Includes compatibility improvements for Homebridge 1.8+ and 2.0 betas
20
20
  - Can optionally expose a schedule switch for each thermostat
21
- - Allows advanced OAuth overrides for long-term API stability
22
21
 
23
22
  ## Compatibility
24
23
 
@@ -45,9 +44,9 @@ The published package name for this maintained fork is `homebridge-nuheat2`. The
45
44
 
46
45
  ## Configuration
47
46
 
48
- Most users should configure the plugin through the custom Homebridge admin UI. It is organized into Account, Accessories, Behavior, Advanced OAuth, and Diagnostics panels and writes the same config keys shown below.
47
+ Most users should configure the plugin through the custom Homebridge admin UI. It is organized into Account, Accessories, Behavior, and Diagnostics panels and writes the same config keys shown below.
49
48
 
50
- Sensitive values are handled deliberately: saved passwords and legacy client secrets are not redisplayed in the UI. Leave those fields blank to keep the saved value, enter a new value to replace it, or use the Clear Overrides button in Advanced OAuth to remove OAuth overrides.
49
+ Sensitive values are handled deliberately: saved passwords are not redisplayed in the UI. Leave the password field blank to keep the saved value, or enter a new value to replace it.
51
50
 
52
51
  The Diagnostics panel summarizes the saved configuration and exposure strategy before restart. It does not make live Nuheat API calls.
53
52
 
@@ -90,9 +89,6 @@ The equivalent JSON looks like this:
90
89
  - `refresh`: Poll interval in seconds, default `60`. Values lower than `30` are raised to `30` to reduce API traffic
91
90
  - `enableNotifications`: Enables Nuheat SignalR notifications for faster updates. Defaults to `true`; set to `false` only while troubleshooting
92
91
  - `debug`: Enables verbose Nuheat API, notification, and accessory logging. Defaults to `false`
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
95
- - `redirectUri`: Optional advanced override for the Nuheat OAuth redirect URI, default `http://localhost`
96
92
 
97
93
  ### Hold Length Behavior
98
94
 
@@ -115,9 +111,7 @@ Nuheat's public OpenAPI documentation indicates that third-party developers shou
115
111
  - [Nuheat OpenAPI docs](https://api.mynuheat.com/)
116
112
  - [Nuheat API access request page](https://www.nuheat.com/openapi)
117
113
 
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.
114
+ 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 and no API key setup is required.
121
115
 
122
116
  ## What's New In This Fork
123
117
 
@@ -121,23 +121,6 @@
121
121
  "type": "boolean",
122
122
  "default": false,
123
123
  "description": "Write detailed Nuheat API and accessory activity to the Homebridge log."
124
- },
125
- "clientId": {
126
- "title": "Nuheat Client ID",
127
- "type": "string",
128
- "description": "Advanced OAuth override. Leave blank to use the built-in PKCE public client ID."
129
- },
130
- "clientSecret": {
131
- "title": "Nuheat Client Secret (Legacy)",
132
- "type": "string",
133
- "format": "password",
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
- },
136
- "redirectUri": {
137
- "title": "Nuheat Redirect URI",
138
- "type": "string",
139
- "default": "http://localhost",
140
- "description": "Advanced OAuth redirect URI. The default is http://localhost."
141
124
  }
142
125
  }
143
126
  },
@@ -171,18 +154,6 @@
171
154
  "enableNotifications",
172
155
  "debug"
173
156
  ]
174
- },
175
- {
176
- "type": "fieldset",
177
- "title": "Advanced OAuth",
178
- "expandable": true,
179
- "expanded": false,
180
- "description": "Only change these fields when testing alternate Nuheat-issued API credentials.",
181
- "items": [
182
- "clientId",
183
- "clientSecret",
184
- "redirectUri"
185
- ]
186
157
  }
187
158
  ],
188
159
  "form": null,
@@ -22,12 +22,6 @@
22
22
  </div>
23
23
 
24
24
  <div class="settings-grid">
25
- <label class="field">
26
- <span>Platform Name</span>
27
- <input id="name" type="text" placeholder="NuHeat" />
28
- <small>Name shown in Homebridge logs for this platform instance.</small>
29
- </label>
30
-
31
25
  <label class="field">
32
26
  <span>MyNuheat Email</span>
33
27
  <input id="email" type="email" placeholder="name@example.com" />
@@ -168,56 +162,6 @@
168
162
  </div>
169
163
  </section>
170
164
 
171
- <section class="panel">
172
- <div class="section-header">
173
- <div>
174
- <h2>Advanced OAuth</h2>
175
- <p class="help">
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
- </p>
180
- </div>
181
- <span id="oauth-status" class="status-pill good">Built-in PKCE</span>
182
- </div>
183
-
184
- <div class="settings-grid">
185
- <label class="field">
186
- <span>Nuheat Client ID</span>
187
- <input id="client-id" type="text" placeholder="Optional client ID" />
188
- <small>
189
- Advanced override. Blank uses homebridge-nuheat2_260421.
190
- </small>
191
- </label>
192
-
193
- <label id="client-secret-row" class="field">
194
- <span>Nuheat Client Secret (Legacy)</span>
195
- <input
196
- id="client-secret"
197
- type="password"
198
- placeholder="Leave blank to keep existing"
199
- />
200
- <small>
201
- Leave blank for PKCE public-client auth. Only use this for legacy
202
- confidential-client credentials.
203
- </small>
204
- </label>
205
-
206
- <label class="field">
207
- <span>Nuheat Redirect URI</span>
208
- <input id="redirect-uri" type="text" placeholder="http://localhost" />
209
- <small>Defaults to http://localhost.</small>
210
- </label>
211
- </div>
212
-
213
- <div class="actions">
214
- <button id="save-oauth" class="primary">Save OAuth Settings</button>
215
- <button id="clear-oauth" class="secondary" type="button">
216
- Clear Overrides
217
- </button>
218
- </div>
219
- </section>
220
-
221
165
  <section class="panel">
222
166
  <div class="section-header">
223
167
  <div>
@@ -1,8 +1,6 @@
1
1
  const PLATFORM_NAME = "NuHeat";
2
- const BUILT_IN_CLIENT_ID = "homebridge-nuheat2_260421";
3
2
 
4
3
  const elements = {
5
- name: document.getElementById("name"),
6
4
  email: document.getElementById("email"),
7
5
  password: document.getElementById("password"),
8
6
  devicesList: document.getElementById("devices-list"),
@@ -15,20 +13,14 @@ const elements = {
15
13
  refresh: document.getElementById("refresh"),
16
14
  enableNotifications: document.getElementById("enable-notifications"),
17
15
  debug: document.getElementById("debug"),
18
- clientId: document.getElementById("client-id"),
19
- clientSecret: document.getElementById("client-secret"),
20
- redirectUri: document.getElementById("redirect-uri"),
21
16
  saveAccount: document.getElementById("save-account"),
22
17
  addDevice: document.getElementById("add-device"),
23
18
  addGroup: document.getElementById("add-group"),
24
19
  saveAccessories: document.getElementById("save-accessories"),
25
20
  saveBehavior: document.getElementById("save-behavior"),
26
- saveOauth: document.getElementById("save-oauth"),
27
- clearOauth: document.getElementById("clear-oauth"),
28
21
  authStatus: document.getElementById("auth-status"),
29
22
  accessoryStatus: document.getElementById("accessory-status"),
30
23
  behaviorStatus: document.getElementById("behavior-status"),
31
- oauthStatus: document.getElementById("oauth-status"),
32
24
  toastContainer: document.getElementById("toast-container"),
33
25
  refreshDiagnostics: document.getElementById("refresh-diagnostics"),
34
26
  diagnosticsSummary: document.getElementById("diagnostics-summary"),
@@ -40,7 +32,6 @@ const state = {
40
32
  configs: [],
41
33
  config: null,
42
34
  hasPassword: false,
43
- hasClientSecret: false,
44
35
  };
45
36
 
46
37
  function showToast(type, message) {
@@ -118,7 +109,6 @@ function createDefaultConfig() {
118
109
  }
119
110
 
120
111
  function renderConfig(config) {
121
- elements.name.value = config.name || "NuHeat";
122
112
  elements.email.value = config.email || config.Email || "";
123
113
  elements.password.value = "";
124
114
  state.hasPassword = Boolean(config.password);
@@ -138,27 +128,13 @@ function renderConfig(config) {
138
128
  elements.enableNotifications.checked = config.enableNotifications !== false;
139
129
  elements.debug.checked = Boolean(config.debug);
140
130
 
141
- elements.clientId.value = config.clientId || "";
142
- elements.clientSecret.value = "";
143
- state.hasClientSecret = Boolean(
144
- config.clientSecret && config.clientId !== BUILT_IN_CLIENT_ID,
145
- );
146
- elements.clientSecret.placeholder = state.hasClientSecret
147
- ? "Saved secret (leave blank to keep)"
148
- : "Optional client secret";
149
- elements.redirectUri.value = config.redirectUri || "http://localhost";
150
-
151
131
  updateStatuses();
152
132
  renderDiagnostics();
153
133
  }
154
134
 
155
135
  function renderRows(container, type, items) {
156
136
  container.innerHTML = "";
157
- const normalizedItems = Array.isArray(items) ? items : [];
158
- const visibleItems = normalizedItems.filter((item) => {
159
- const value = type === "device" ? item.serialNumber : item.groupName;
160
- return typeof value === "string" && value.trim().length > 0;
161
- });
137
+ const visibleItems = normalizeConfigRows(type, items);
162
138
 
163
139
  if (visibleItems.length === 0) {
164
140
  const empty = document.createElement("div");
@@ -175,12 +151,44 @@ function renderRows(container, type, items) {
175
151
  addRow(
176
152
  container,
177
153
  type,
178
- type === "device" ? item.serialNumber : item.groupName,
179
- Boolean(item.disabled),
154
+ item.value,
155
+ item.disabled,
180
156
  );
181
157
  });
182
158
  }
183
159
 
160
+ function normalizeConfigRows(type, items) {
161
+ const normalizedItems = Array.isArray(items) ? items : [];
162
+ return normalizedItems
163
+ .map((item) => ({
164
+ value: getConfigRowValue(type, item),
165
+ disabled: typeof item === "object" && item !== null
166
+ ? Boolean(item.disabled)
167
+ : false,
168
+ }))
169
+ .filter((item) => item.value.length > 0);
170
+ }
171
+
172
+ function getConfigRowValue(type, item) {
173
+ if (typeof item === "string" || typeof item === "number") {
174
+ return String(item).trim();
175
+ }
176
+
177
+ if (!item || typeof item !== "object") {
178
+ return "";
179
+ }
180
+
181
+ const keys = type === "device"
182
+ ? ["serialNumber", "SerialNumber", "serial", "Serial", "deviceId", "DeviceId"]
183
+ : ["groupName", "GroupName", "name", "Name"];
184
+
185
+ const value = keys
186
+ .map((key) => item[key])
187
+ .find((candidate) => typeof candidate === "string" || typeof candidate === "number");
188
+
189
+ return value === undefined ? "" : String(value).trim();
190
+ }
191
+
184
192
  function addRow(container, type, value = "", disabled = false) {
185
193
  const empty = container.querySelector(".empty-list");
186
194
  if (empty) {
@@ -246,7 +254,6 @@ function collectRows(container, key) {
246
254
  }
247
255
 
248
256
  async function saveAccount() {
249
- const name = elements.name.value.trim() || "NuHeat";
250
257
  const email = elements.email.value.trim();
251
258
  const password = elements.password.value;
252
259
 
@@ -264,7 +271,6 @@ async function saveAccount() {
264
271
 
265
272
  const patch = {
266
273
  platform: PLATFORM_NAME,
267
- name,
268
274
  email,
269
275
  Email: undefined,
270
276
  };
@@ -312,57 +318,6 @@ async function saveBehavior() {
312
318
  showToast("success", "Behavior settings saved.");
313
319
  }
314
320
 
315
- async function saveOauth() {
316
- const clientId = elements.clientId.value.trim();
317
- const clientSecret = elements.clientSecret.value;
318
- const redirectUri = elements.redirectUri.value.trim();
319
- const usesBuiltInClient = !clientId || clientId === BUILT_IN_CLIENT_ID;
320
-
321
- if (!clientId && clientSecret) {
322
- showToast("error", "A client secret requires a Nuheat client ID.");
323
- elements.clientId.focus();
324
- return;
325
- }
326
-
327
- const patch = {
328
- clientId: usesBuiltInClient ? undefined : clientId,
329
- redirectUri: redirectUri && redirectUri !== "http://localhost" ? redirectUri : undefined,
330
- };
331
-
332
- if (clientSecret && !usesBuiltInClient) {
333
- patch.clientSecret = clientSecret;
334
- } else if (usesBuiltInClient || clientId !== (state.config?.clientId || "")) {
335
- patch.clientSecret = undefined;
336
- }
337
-
338
- await persistPatch(patch);
339
- state.hasClientSecret = Boolean(state.config?.clientSecret);
340
- elements.clientId.value = state.config?.clientId || "";
341
- elements.clientSecret.value = "";
342
- elements.clientSecret.placeholder = state.hasClientSecret
343
- ? "Saved secret (leave blank to keep)"
344
- : "Optional client secret";
345
- updateStatuses();
346
- renderDiagnostics();
347
- showToast("success", "OAuth settings saved.");
348
- }
349
-
350
- async function clearOauth() {
351
- await persistPatch({
352
- clientId: undefined,
353
- clientSecret: undefined,
354
- redirectUri: undefined,
355
- });
356
- state.hasClientSecret = false;
357
- elements.clientId.value = "";
358
- elements.clientSecret.value = "";
359
- elements.clientSecret.placeholder = "Optional client secret";
360
- elements.redirectUri.value = "http://localhost";
361
- updateStatuses();
362
- renderDiagnostics();
363
- showToast("success", "OAuth overrides cleared.");
364
- }
365
-
366
321
  async function persistPatch(patch) {
367
322
  await withSpinner(async () => {
368
323
  if (!state.config) {
@@ -395,7 +350,6 @@ function updateStatuses() {
395
350
  updateAuthStatus();
396
351
  updateAccessoryStatus();
397
352
  updateBehaviorStatus();
398
- updateOauthStatus();
399
353
  }
400
354
 
401
355
  function updateAuthStatus() {
@@ -441,31 +395,6 @@ function updateBehaviorStatus() {
441
395
  );
442
396
  }
443
397
 
444
- function updateOauthStatus() {
445
- const clientId = elements.clientId.value.trim();
446
- const hasClientId = clientId.length > 0 && clientId !== BUILT_IN_CLIENT_ID;
447
- const hasClientSecret = hasUsableClientSecret(clientId);
448
- const text = hasClientId
449
- ? hasClientSecret
450
- ? "Custom legacy OAuth"
451
- : "Custom PKCE"
452
- : "Built-in PKCE";
453
- setStatus(elements.oauthStatus, true, text);
454
- }
455
-
456
- function hasUsableClientSecret(clientId) {
457
- if (!clientId || clientId === BUILT_IN_CLIENT_ID) {
458
- return false;
459
- }
460
-
461
- if (elements.clientSecret.value) {
462
- return true;
463
- }
464
-
465
- const savedClientId = state.config?.clientId || "";
466
- return Boolean(clientId && state.hasClientSecret && clientId === savedClientId);
467
- }
468
-
469
398
  function setStatus(element, isGood, text) {
470
399
  element.textContent = text;
471
400
  element.classList.toggle("good", Boolean(isGood));
@@ -488,10 +417,9 @@ function renderDiagnostics() {
488
417
 
489
418
  addDiagnosticCard("Account", [
490
419
  ["Platform", PLATFORM_NAME],
491
- ["Name", config.name || "NuHeat"],
492
420
  ["Email", config.email || "not configured"],
493
421
  ["Password", state.hasPassword || elements.password.value ? "saved" : "missing"],
494
- ["OAuth Mode", getOauthMode(config)],
422
+ ["API Access", "built-in Nuheat PKCE client"],
495
423
  ]);
496
424
 
497
425
  addDiagnosticCard("Accessories", [
@@ -537,7 +465,6 @@ function addDiagnosticCard(title, rows) {
537
465
  function getDraftConfig() {
538
466
  return {
539
467
  ...(state.config || createDefaultConfig()),
540
- name: elements.name.value.trim() || "NuHeat",
541
468
  email: elements.email.value.trim(),
542
469
  devices: collectRows(elements.devicesList, "serialNumber"),
543
470
  groups: collectRows(elements.groupsList, "groupName"),
@@ -549,8 +476,6 @@ function getDraftConfig() {
549
476
  refresh: normalizeRefresh(elements.refresh.value),
550
477
  enableNotifications: Boolean(elements.enableNotifications.checked),
551
478
  debug: Boolean(elements.debug.checked),
552
- clientId: elements.clientId.value.trim(),
553
- redirectUri: elements.redirectUri.value.trim() || "http://localhost",
554
479
  };
555
480
  }
556
481
 
@@ -586,19 +511,6 @@ function getHoldSummary(value) {
586
511
  return `${holdLength} minute timed hold`;
587
512
  }
588
513
 
589
- function getOauthMode(config) {
590
- if (!config.clientId || config.clientId === BUILT_IN_CLIENT_ID) {
591
- return "built-in PKCE public client";
592
- }
593
-
594
- if (config.clientId && hasUsableClientSecret(config.clientId)) {
595
- return "configured confidential client";
596
- }
597
- if (config.clientId) {
598
- return "configured PKCE public client";
599
- }
600
- }
601
-
602
514
  function normalizeHoldLength(value) {
603
515
  const parsed = Number.parseInt(value, 10);
604
516
  if (!Number.isFinite(parsed)) {
@@ -640,12 +552,6 @@ function bindEvents() {
640
552
  elements.saveBehavior.addEventListener("click", () => {
641
553
  saveBehavior().catch(() => showToast("error", "Failed to save behavior."));
642
554
  });
643
- elements.saveOauth.addEventListener("click", () => {
644
- saveOauth().catch(() => showToast("error", "Failed to save OAuth settings."));
645
- });
646
- elements.clearOauth.addEventListener("click", () => {
647
- clearOauth().catch(() => showToast("error", "Failed to clear OAuth settings."));
648
- });
649
555
  elements.addDevice.addEventListener("click", () => {
650
556
  addRow(elements.devicesList, "device");
651
557
  });
@@ -655,7 +561,6 @@ function bindEvents() {
655
561
  elements.refreshDiagnostics.addEventListener("click", renderDiagnostics);
656
562
 
657
563
  [
658
- elements.name,
659
564
  elements.email,
660
565
  elements.password,
661
566
  elements.autoPopulateAwayModeSwitches,
@@ -664,9 +569,6 @@ function bindEvents() {
664
569
  elements.refresh,
665
570
  elements.enableNotifications,
666
571
  elements.debug,
667
- elements.clientId,
668
- elements.clientSecret,
669
- elements.redirectUri,
670
572
  ].forEach((element) => {
671
573
  element.addEventListener("input", () => {
672
574
  updateStatuses();
@@ -124,8 +124,8 @@ select {
124
124
  width: 100%;
125
125
  min-height: 38px;
126
126
  padding: 0.45rem 0.75rem;
127
- color: inherit;
128
- background: var(--bs-body-bg, var(--plugin-control-surface));
127
+ color: currentColor;
128
+ background-color: var(--plugin-control-surface);
129
129
  border: 1px solid var(--plugin-border);
130
130
  border-radius: 0.375rem;
131
131
  font-family: var(--plugin-font-family);
@@ -293,9 +293,23 @@ button.secondary:hover {
293
293
 
294
294
  .config-row {
295
295
  display: grid;
296
- grid-template-columns: minmax(0, 1fr) auto;
296
+ grid-template-columns: 1fr;
297
297
  gap: 10px;
298
+ align-items: stretch;
299
+ background: var(--plugin-control-surface);
300
+ border: 1px solid var(--plugin-border);
301
+ border-radius: 8px;
302
+ padding: 12px;
303
+ }
304
+
305
+ .config-row .field {
306
+ min-width: 0;
307
+ }
308
+
309
+ .config-row .row-actions {
310
+ justify-content: flex-start;
298
311
  align-items: center;
312
+ flex-wrap: wrap;
299
313
  }
300
314
 
301
315
  .row-checkbox {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-nuheat2",
3
- "version": "1.2.17",
3
+ "version": "1.2.19",
4
4
  "description": "Homebridge Platform for NuHeat Signature Thermostats",
5
5
  "main": "index.js",
6
6
  "scripts": {