homebridge-nuheat2 1.2.9 → 1.2.11

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,23 @@ All notable changes to this project should be documented in this file
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.11] - 2026-04-21
8
+
9
+ ### Fixed
10
+
11
+ - Relax the optional `devices` and `groups` array item schema so Homebridge Config UI no longer shows validation errors for blank rows
12
+ - Ignore blank `devices` rows at runtime so they do not interfere with thermostat auto-discovery
13
+
14
+ ## [1.2.10] - 2026-04-21
15
+
16
+ ### Added
17
+
18
+ - Add non-sensitive OAuth debug logging for configured client ID, requested scopes, consent handling, and refresh-token rotation during local API credential validation
19
+
20
+ ### Changed
21
+
22
+ - Request the full issued Nuheat OAuth scope set: `openapi openid profile offline_access`
23
+
7
24
  ## [1.2.9] - 2026-04-15
8
25
 
9
26
  ### Fixed
package/README.md CHANGED
@@ -67,11 +67,11 @@ Most users should configure the plugin through Homebridge Config UI X, but the e
67
67
  - `email`: MyNuheat account email address
68
68
  - `Email`: Legacy alias still accepted for backward compatibility, but `email` is the preferred documented field
69
69
  - `password`: MyNuheat account password
70
- - `devices`: Optional list of thermostats to expose. If omitted or empty, every thermostat on the account will be discovered automatically
70
+ - `devices`: Optional list of thermostats to expose. If omitted or empty, every thermostat on the account will be discovered automatically. Blank rows in the UI are ignored
71
71
  - `serialNumber`: Thermostat serial number from MyNuheat
72
72
  - `autoPopulateAwayModeSwitches`: Automatically expose away-mode switches for all groups on the account
73
73
  - `exposeScheduleSwitches`: Optionally expose a switch per thermostat that reflects whether the thermostat is following its schedule and can be turned on to resume the schedule
74
- - `groups`: Optional allow-list of groups to expose as away-mode switches. This only affects group/away-mode accessories
74
+ - `groups`: Optional allow-list of groups to expose as away-mode switches. This only affects group/away-mode accessories. Blank rows in the UI are ignored
75
75
  - `groupName`: Group name as shown in MyNuheat
76
76
  - `holdLength`: Hold duration in minutes
77
77
  - `refresh`: Poll interval in seconds, default `60`
@@ -138,10 +138,12 @@ npm test
138
138
  GitHub Actions now handles two jobs for this repository:
139
139
 
140
140
  - `.github/workflows/ci.yml` runs `npm ci`, `npm run typecheck`, and `npm test` on pushes and pull requests across Node 20, 22, and 24
141
- - `.github/workflows/publish.yml` runs on pushes to `master` when `package.json` changes, re-runs the checks on Node 24, and publishes to npm only when the `package.json` version is not already on the registry
141
+ - `.github/workflows/publish.yml` runs on pushes to `master` when `package.json` changes, re-runs the checks on Node 24, publishes to npm only when the `package.json` version is not already on the registry, and creates or updates the matching GitHub Release
142
142
 
143
143
  The publish workflow also maps prerelease versions to npm dist-tags automatically. For example, `1.2.7-beta.1` publishes with the `beta` tag, while stable versions publish to `latest`.
144
144
 
145
+ Release notes are expected in `docs/release-notes/<version>.md`. The publish workflow will fail if that file is missing for the version in `package.json`, which makes the GitHub Release step part of the normal release checklist instead of a manual follow-up.
146
+
145
147
  ### Recommended npm Setup
146
148
 
147
149
  Use npm trusted publishing rather than a long-lived automation token.
@@ -43,11 +43,9 @@
43
43
  "devices": {
44
44
  "title": "Devices",
45
45
  "type": "array",
46
+ "description": "Optional allow-list of thermostats to expose. Leave empty to auto-discover all thermostats on the account.",
46
47
  "items": {
47
48
  "type": "object",
48
- "required": [
49
- "serialNumber"
50
- ],
51
49
  "properties": {
52
50
  "serialNumber": {
53
51
  "title": "Serial Number",
@@ -72,11 +70,9 @@
72
70
  "groups": {
73
71
  "title": "Groups",
74
72
  "type": "array",
73
+ "description": "Optional allow-list of groups to expose as away-mode switches. Leave empty unless you want to add specific groups manually.",
75
74
  "items": {
76
75
  "type": "object",
77
- "required": [
78
- "groupName"
79
- ],
80
76
  "properties": {
81
77
  "groupName": {
82
78
  "title": "Group Name",
package/index.js CHANGED
@@ -74,6 +74,11 @@ class NuHeatPlatform {
74
74
  typeof group.groupName === "string" &&
75
75
  group.groupName.trim().length > 0);
76
76
  }
77
+ getConfiguredDevices() {
78
+ return (this.config.devices || []).filter((device) => !!device &&
79
+ typeof device.serialNumber === "string" &&
80
+ device.serialNumber.trim().length > 0);
81
+ }
77
82
  shouldManageGroups() {
78
83
  return (!!this.config.autoPopulateAwayModeSwitches ||
79
84
  this.getConfiguredGroups().length > 0);
@@ -154,7 +159,7 @@ class NuHeatPlatform {
154
159
  }));
155
160
  }
156
161
  async setupThermostats() {
157
- const deviceArray = this.config.devices || [];
162
+ const deviceArray = this.getConfiguredDevices();
158
163
  const response = await this.NuHeatAPI.refreshThermostats();
159
164
  if (!response || !Array.isArray(response)) {
160
165
  this.log.error("Error getting data from NuHeatAPI");
package/lib/NuHeatAPI.js CHANGED
@@ -8,6 +8,7 @@ const isRedirect = fetchModule.isRedirect;
8
8
  const { parse } = htmlParser;
9
9
  const settings_1 = require("./settings");
10
10
  const NuHeatModels_1 = require("./NuHeatModels");
11
+ const OAUTH_SCOPES = ["openapi", "openid", "profile", "offline_access"];
11
12
  class NuHeatAPI {
12
13
  email;
13
14
  password;
@@ -56,6 +57,16 @@ class NuHeatAPI {
56
57
  if (this.usingFallbackCredentials) {
57
58
  this.log.warn("NuHeatAPI: Using built-in OAuth client credentials. Request your own Nuheat API client for long-term reliability.");
58
59
  }
60
+ else {
61
+ this.log.info("NuHeatAPI: Using configured OAuth client ID " + this.oauthClientId + ".");
62
+ }
63
+ this.log.debug("NuHeatAPI: OAuth redirect URI " +
64
+ this.oauthRedirectUri +
65
+ ". Requested scopes: " +
66
+ this.getRequestedScope());
67
+ }
68
+ getRequestedScope() {
69
+ return OAUTH_SCOPES.join(" ");
59
70
  }
60
71
  async setAwayMode(groupId, awayMode) {
61
72
  const callURL = "https://api.mynuheat.com/api/v1/Group";
@@ -236,7 +247,9 @@ class NuHeatAPI {
236
247
  authEndpoint.searchParams.set("response_type", "code");
237
248
  authEndpoint.searchParams.set("client_id", this.oauthClientId);
238
249
  authEndpoint.searchParams.set("redirect_uri", this.oauthRedirectUri);
239
- authEndpoint.searchParams.set("scope", "openapi openid offline_access");
250
+ authEndpoint.searchParams.set("scope", this.getRequestedScope());
251
+ this.log.debug("NuHeatAPI: Requesting OAuth authorization page with scopes: " +
252
+ this.getRequestedScope());
240
253
  const response = await this.fetch(authEndpoint.toString(), {
241
254
  redirect: "follow",
242
255
  });
@@ -322,9 +335,11 @@ class NuHeatAPI {
322
335
  RememberConsent: "true",
323
336
  __RequestVerificationToken: requestVerificationToken,
324
337
  });
338
+ this.log.debug("NuHeatAPI: OAuth consent required. Confirming scopes: " +
339
+ this.getRequestedScope());
325
340
  const response = await this.fetch(settings_1.NUHEAT_API_CONSENT_URI, {
326
341
  body: loginBody.toString() +
327
- "&ScopesConsented=openid&ScopesConsented=openapi&ScopesConsented=offline_access",
342
+ OAUTH_SCOPES.map((scope) => "&ScopesConsented=" + scope).join(""),
328
343
  headers: {
329
344
  "Content-Type": "application/x-www-form-urlencoded",
330
345
  Cookie: cookie + "; " + sessionCookie,
@@ -401,7 +416,12 @@ class NuHeatAPI {
401
416
  return null;
402
417
  }
403
418
  this.tokenScope = redirectUrl.searchParams.get("scope") ?? "";
404
- return (await response.json());
419
+ const token = (await response.json());
420
+ this.log.debug("NuHeatAPI: OAuth access token received. Granted scopes: " +
421
+ (token.scope || this.tokenScope || "unknown") +
422
+ ". Refresh token present: " +
423
+ String(!!token.refresh_token));
424
+ return token;
405
425
  }
406
426
  return null;
407
427
  }
@@ -431,12 +451,19 @@ class NuHeatAPI {
431
451
  this.refreshToken = token.refresh_token;
432
452
  this.tokenScope = token.scope ?? this.tokenScope;
433
453
  this.tokenType = token.token_type;
454
+ const refreshTokenRotated = token.refresh_token !== this.refreshToken;
434
455
  this.refreshInterval -= 420;
435
456
  if (this.refreshInterval < 300) {
436
457
  this.refreshInterval = 300;
437
458
  }
438
459
  this.headers.set("Authorization", token.token_type + " " + token.access_token);
439
- this.log.debug("NuHeatAPI: Successfully refreshed the NuHeat API access token.");
460
+ this.log.debug("NuHeatAPI: Successfully refreshed the NuHeat API access token. Scope: " +
461
+ this.tokenScope +
462
+ ". Refresh token rotated: " +
463
+ String(refreshTokenRotated) +
464
+ ". Expires in: " +
465
+ token.expires_in +
466
+ " seconds.");
440
467
  return true;
441
468
  }
442
469
  async returnAccessToken() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-nuheat2",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
4
4
  "description": "Homebridge Platform for NuHeat Signature Thermostats",
5
5
  "main": "index.js",
6
6
  "scripts": {