homebridge-nuheat2 1.2.5 → 1.2.6

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,17 @@ All notable changes to this project should be documented in this file
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.6] - 2026-04-11
8
+
9
+ ### Added
10
+
11
+ - Add an `enableNotifications` config option so SignalR can be disabled during troubleshooting
12
+
13
+ ### Changed
14
+
15
+ - Skip group setup and refresh calls unless away-mode groups are actually configured
16
+ - Improve Nuheat API error logging with request method, endpoint, status code, and response snippets
17
+
7
18
  ## [1.2.5] - 2026-04-11
8
19
 
9
20
  ### Changed
@@ -95,12 +95,17 @@
95
95
  "placeholder": 60,
96
96
  "minimum": 1
97
97
  },
98
- "debug": {
99
- "title": "Enable debug logs",
100
- "type": "boolean"
101
- }
102
- }
103
- },
98
+ "debug": {
99
+ "title": "Enable debug logs",
100
+ "type": "boolean"
101
+ },
102
+ "enableNotifications": {
103
+ "title": "Enable Nuheat notifications (SignalR)",
104
+ "type": "boolean",
105
+ "description": "Disable this to use REST polling only while troubleshooting notification or API issues."
106
+ }
107
+ }
108
+ },
104
109
  "form": null,
105
110
  "display": null
106
111
  }
package/index.js CHANGED
@@ -21,7 +21,7 @@ module.exports = function (homebridge) {
21
21
  homebridge.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, NuHeatPlatform, true);
22
22
  };
23
23
 
24
- class NuHeatPlatform {
24
+ class NuHeatPlatform {
25
25
  constructor(log, config, api) {
26
26
  if (!config) {
27
27
  log.warn("Ignoring NuHeat Platform setup because it is not configured");
@@ -39,10 +39,10 @@ class NuHeatPlatform {
39
39
 
40
40
  this.config = config;
41
41
  this.config.email = this.config.Email || this.config.email;
42
- this.config.holdLength = Math.min(
43
- 1440,
44
- Math.max(0, this.config.holdLength || 1440),
45
- );
42
+ this.config.holdLength = Math.min(
43
+ 1440,
44
+ Math.max(0, this.config.holdLength || 1440),
45
+ );
46
46
  this.api = api;
47
47
  this.accessories = [];
48
48
  this.log = new logger.Logger(log, this.config.debug || false);
@@ -63,11 +63,27 @@ class NuHeatPlatform {
63
63
  }
64
64
  }
65
65
 
66
- configureAccessory(accessory) {
67
- this.accessories.push({ uuid: accessory.UUID, accessory });
68
- }
69
-
70
- async setupPlatform() {
66
+ configureAccessory(accessory) {
67
+ this.accessories.push({ uuid: accessory.UUID, accessory });
68
+ }
69
+
70
+ getConfiguredGroups() {
71
+ return (this.config.groups || []).filter(
72
+ (group) =>
73
+ group &&
74
+ typeof group.groupName === "string" &&
75
+ group.groupName.trim().length > 0,
76
+ );
77
+ }
78
+
79
+ shouldManageGroups() {
80
+ return (
81
+ this.config.autoPopulateAwayModeSwitches ||
82
+ this.getConfiguredGroups().length > 0
83
+ );
84
+ }
85
+
86
+ async setupPlatform() {
71
87
  if (this.disabled) {
72
88
  return;
73
89
  }
@@ -98,15 +114,19 @@ class NuHeatPlatform {
98
114
  clearInterval(this.refreshTimer);
99
115
  }
100
116
 
101
- this.refreshTimer = setInterval(
102
- this.refreshAccessories.bind(this),
103
- (this.config.refresh || 60) * 1000,
104
- );
105
-
106
- if (!this.NuHeatListener) {
107
- this.NuHeatListener = new NuHeatListener(this.NuHeatAPI, this);
108
- this.NuHeatListener.connect();
109
- }
117
+ this.refreshTimer = setInterval(
118
+ this.refreshAccessories.bind(this),
119
+ (this.config.refresh || 60) * 1000,
120
+ );
121
+
122
+ if (this.config.enableNotifications === false) {
123
+ this.log.info(
124
+ "NuHeat notifications are disabled. Using REST polling only.",
125
+ );
126
+ } else if (!this.NuHeatListener) {
127
+ this.NuHeatListener = new NuHeatListener(this.NuHeatAPI, this);
128
+ this.NuHeatListener.connect();
129
+ }
110
130
  } else {
111
131
  this.log.error(
112
132
  "Unable to acquire an access token. We will try again later.",
@@ -118,11 +138,11 @@ class NuHeatPlatform {
118
138
  }
119
139
  }
120
140
 
121
- async setupGroups() {
122
- const groupArray = this.config.groups || [];
123
- if (!(this.config.autoPopulateAwayModeSwitches || groupArray.length > 0)) {
124
- return;
125
- }
141
+ async setupGroups() {
142
+ const groupArray = this.getConfiguredGroups();
143
+ if (!this.shouldManageGroups()) {
144
+ return;
145
+ }
126
146
 
127
147
  const response = await this.NuHeatAPI.refreshGroups();
128
148
  if (!response) {
@@ -317,14 +337,20 @@ class NuHeatPlatform {
317
337
  });
318
338
  }
319
339
 
320
- async refreshAccessories() {
321
- await this.refreshGroups();
322
- await this.refreshThermostats();
323
- }
324
-
325
- async refreshGroups() {
326
- this.log.debug("Trying to refresh groups.");
327
- const response = await this.NuHeatAPI.refreshGroups();
340
+ async refreshAccessories() {
341
+ if (this.shouldManageGroups()) {
342
+ await this.refreshGroups();
343
+ }
344
+ await this.refreshThermostats();
345
+ }
346
+
347
+ async refreshGroups() {
348
+ if (!this.shouldManageGroups()) {
349
+ return true;
350
+ }
351
+
352
+ this.log.debug("Trying to refresh groups.");
353
+ const response = await this.NuHeatAPI.refreshGroups();
328
354
 
329
355
  if (!response) {
330
356
  this.log.error("Error getting data from NuHeatAPI in group refresh");
package/lib/NuHeatAPI.js CHANGED
@@ -24,7 +24,7 @@ const {
24
24
  normalizeThermostat,
25
25
  } = require("./NuHeatModels");
26
26
 
27
- module.exports = class NuHeatAPI {
27
+ module.exports = class NuHeatAPI {
28
28
  constructor(email, password, log, options = {}) {
29
29
  this.email = email;
30
30
  this.password = password;
@@ -290,7 +290,7 @@ module.exports = class NuHeatAPI {
290
290
  );
291
291
  }
292
292
 
293
- async makeAPICall(callURL, callOptions = {}, options = {}) {
293
+ async makeAPICall(callURL, callOptions = {}, options = {}) {
294
294
  // Validate and potentially refresh our access token.
295
295
  if (!(await this.refreshAccessToken())) {
296
296
  return false;
@@ -318,8 +318,25 @@ module.exports = class NuHeatAPI {
318
318
  return options.normalize(returnedData);
319
319
  }
320
320
 
321
- return returnedData;
322
- }
321
+ return returnedData;
322
+ }
323
+
324
+ describeRequest(url, options = {}) {
325
+ return (options.method || "GET") + " " + url;
326
+ }
327
+
328
+ async readResponseBody(response) {
329
+ try {
330
+ const responseText = await response.text();
331
+ if (!responseText) {
332
+ return "";
333
+ }
334
+
335
+ return responseText.replace(/\s+/g, " ").trim().slice(0, 300);
336
+ } catch {
337
+ return "";
338
+ }
339
+ }
323
340
 
324
341
  // Retrieve the NuHeat OAuth authorization page to prepare to login.
325
342
  async oauthGetAuthPage() {
@@ -760,51 +777,67 @@ module.exports = class NuHeatAPI {
760
777
  }
761
778
 
762
779
  // Utility to let us streamline error handling and return checking from the NuHeat API.
763
- async fetch(url, options = {}, decodeResponse = true, isRetry = false) {
780
+ async fetch(url, options = {}, decodeResponse = true, isRetry = false) {
764
781
  // Set our headers.
765
782
  if (!options.headers) {
766
783
  options.headers = this.headers;
767
- }
768
- try {
769
- let response = await fetch(url, options);
770
- // The caller will sort through responses instead of us.
771
- if (!decodeResponse) {
772
- return response;
773
- }
774
- // Bad form data submitted.
775
- if (response.status === 400) {
776
- this.log.error(
777
- "NuHeatAPI: Invalid call. Data submitted doesn't seem right",
778
- );
779
- return null;
780
- // Bad username and password.
781
- } else if (response.status === 401) {
782
- this.log.error(
783
- "NuHeatAPI: Invalid NuHeat credentials given. Check your login and password.",
784
- );
785
- return null;
786
- // Error on the NuHeat side.
787
- } else if (response.status === 500) {
788
- this.log.error("NuHeatAPI: NuHeat had an internal server error.");
789
- if (isRetry) {
790
- return null;
791
- } else {
792
- this.log.error("NuHeatAPI: Trying again.");
793
- return this.fetch(url, options, decodeResponse, true);
794
- }
795
- }
796
- // Some other unknown error occurred.
797
- if (!response.ok && !isRedirect(response.status)) {
798
- this.log.error(
799
- "NuHeatAPI: " +
800
- url +
801
- " Error: " +
802
- response.status +
803
- " " +
804
- response.statusText,
805
- );
806
- return null;
807
- }
784
+ }
785
+ try {
786
+ const requestDescription = this.describeRequest(url, options);
787
+ let response = await fetch(url, options);
788
+ // The caller will sort through responses instead of us.
789
+ if (!decodeResponse) {
790
+ return response;
791
+ }
792
+ // Bad form data submitted.
793
+ if (response.status === 400) {
794
+ const responseBody = await this.readResponseBody(response);
795
+ this.log.error(
796
+ "NuHeatAPI: " +
797
+ requestDescription +
798
+ " failed with 400 Bad Request." +
799
+ (responseBody ? " Response body: " + responseBody : ""),
800
+ );
801
+ return null;
802
+ // Bad username and password.
803
+ } else if (response.status === 401) {
804
+ this.log.error(
805
+ "NuHeatAPI: " +
806
+ requestDescription +
807
+ " failed with 401 Unauthorized. Check your NuHeat credentials.",
808
+ );
809
+ return null;
810
+ // Error on the NuHeat side.
811
+ } else if (response.status === 500) {
812
+ const responseBody = await this.readResponseBody(response);
813
+ this.log.error(
814
+ "NuHeatAPI: " +
815
+ requestDescription +
816
+ " failed with 500 Internal Server Error." +
817
+ (responseBody ? " Response body: " + responseBody : ""),
818
+ );
819
+ if (isRetry) {
820
+ return null;
821
+ } else {
822
+ this.log.error("NuHeatAPI: Trying again.");
823
+ return this.fetch(url, options, decodeResponse, true);
824
+ }
825
+ }
826
+ // Some other unknown error occurred.
827
+ if (!response.ok && !isRedirect(response.status)) {
828
+ const responseBody = await this.readResponseBody(response);
829
+ this.log.error(
830
+ "NuHeatAPI: " +
831
+ requestDescription +
832
+ " failed with " +
833
+ response.status +
834
+ " " +
835
+ response.statusText +
836
+ "." +
837
+ (responseBody ? " Response body: " + responseBody : ""),
838
+ );
839
+ return null;
840
+ }
808
841
  return response;
809
842
  } catch (error) {
810
843
  if (error instanceof FetchError) {
@@ -812,16 +845,20 @@ module.exports = class NuHeatAPI {
812
845
  case "ECONNREFUSED":
813
846
  this.log.error("NuHeatAPI: Connection refused.");
814
847
  break;
815
- case "ECONNRESET":
816
- // Retry on connection reset, but no more than once.
817
- if (!isRetry) {
818
- this.log.debug(
819
- "NuHeatAPI: Connection has been reset. Retrying the API action.",
820
- );
821
- return this.fetch(url, options, decodeResponse, true);
822
- }
823
- this.log.error("NuHeatAPI: Connection has been reset.");
824
- break;
848
+ case "ECONNRESET":
849
+ // Retry on connection reset, but no more than once.
850
+ if (!isRetry) {
851
+ this.log.debug(
852
+ "NuHeatAPI: Connection reset during " +
853
+ requestDescription +
854
+ ". Retrying the API action.",
855
+ );
856
+ return this.fetch(url, options, decodeResponse, true);
857
+ }
858
+ this.log.error(
859
+ "NuHeatAPI: Connection reset during " + requestDescription + ".",
860
+ );
861
+ break;
825
862
  case "ENOTFOUND":
826
863
  this.log.error("NuHeatAPI: Hostname or IP address not found.");
827
864
  break;
@@ -830,14 +867,21 @@ module.exports = class NuHeatAPI {
830
867
  "NuHeatAPI: Unable to verify the NuHeat TLS security certificate.",
831
868
  );
832
869
  break;
833
- default:
834
- this.log.error(error.message);
835
- }
836
- } else {
837
- this.log.error("Unknown fetch error: " + error);
838
- }
839
- return null;
840
- }
870
+ default:
871
+ this.log.error(
872
+ "NuHeatAPI: " +
873
+ requestDescription +
874
+ " failed: " +
875
+ error.message,
876
+ );
877
+ }
878
+ } else {
879
+ this.log.error(
880
+ "NuHeatAPI: " + requestDescription + " failed with error: " + error,
881
+ );
882
+ }
883
+ return null;
884
+ }
841
885
  }
842
886
 
843
887
  trimSetCookie(setCookie) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-nuheat2",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "Homebridge Platform for NuHeat Signature Thermostats",
5
5
  "main": "index.js",
6
6
  "scripts": {