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 +11 -0
- package/config.schema.json +11 -6
- package/index.js +58 -32
- package/lib/NuHeatAPI.js +108 -64
- package/package.json +1 -1
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
|
package/config.schema.json
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
|
-
|
|
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 (
|
|
107
|
-
this.
|
|
108
|
-
|
|
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.
|
|
123
|
-
if (!
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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(
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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) {
|