homebridge-nuheat2 1.2.4-beta.0 → 1.2.4
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 -3
- package/README.md +12 -5
- package/config.schema.json +5 -0
- package/index.js +74 -0
- package/lib/NuHeatAPI.js +163 -5
- package/lib/NuHeatListener.js +1 -0
- package/lib/NuHeatModels.js +136 -0
- package/lib/NuHeatScheduleSwitch.js +69 -0
- package/lib/NuHeatThermostat.js +17 -48
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,15 +4,23 @@ All notable changes to this project should be documented in this file
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
## [1.2.4
|
|
7
|
+
## [1.2.4] - 2026-04-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Swagger-aligned internal model helpers for account, thermostat, schedule, group, and energy responses
|
|
12
|
+
- Optional per-thermostat schedule switches that can resume schedule mode from HomeKit
|
|
13
|
+
- Account, schedule, and energy log API helpers for future UI and automation features
|
|
8
14
|
|
|
9
15
|
### Changed
|
|
10
16
|
|
|
11
17
|
- Delay platform startup until Homebridge finishes restoring cached accessories
|
|
12
18
|
- Allow overriding Nuheat OAuth client settings through config or environment variables
|
|
13
19
|
- Improve SignalR reconnection handling and token refresh behavior
|
|
14
|
-
- Add
|
|
15
|
-
- Publish the maintained fork under the
|
|
20
|
+
- Add regression tests and modernize package metadata for current Homebridge releases
|
|
21
|
+
- Publish the maintained fork under the npm package identity `homebridge-nuheat2`
|
|
22
|
+
- Simplify thermostat operating-mode handling to match the documented Nuheat Swagger model
|
|
23
|
+
- Refresh thermostats when Nuheat sends schedule notifications
|
|
16
24
|
|
|
17
25
|
### Fixed
|
|
18
26
|
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ This fork focuses on modernizing the plugin for current Homebridge releases, imp
|
|
|
14
14
|
- Supports permanent, scheduled, and timed holds
|
|
15
15
|
- Uses Nuheat's OAuth-based API instead of legacy site scraping
|
|
16
16
|
- Includes compatibility improvements for Homebridge 1.8+ and 2.0 betas
|
|
17
|
+
- Can optionally expose a schedule switch for each thermostat
|
|
17
18
|
- Allows advanced OAuth overrides for long-term API stability
|
|
18
19
|
|
|
19
20
|
## Compatibility
|
|
@@ -51,6 +52,7 @@ Most users should configure the plugin through Homebridge Config UI X, but the e
|
|
|
51
52
|
"password": "password123",
|
|
52
53
|
"devices": [{ "serialNumber": "1111111" }, { "serialNumber": "2222222" }],
|
|
53
54
|
"autoPopulateAwayModeSwitches": true,
|
|
55
|
+
"exposeScheduleSwitches": false,
|
|
54
56
|
"holdLength": 1440,
|
|
55
57
|
"refresh": 60
|
|
56
58
|
}
|
|
@@ -61,16 +63,18 @@ Most users should configure the plugin through Homebridge Config UI X, but the e
|
|
|
61
63
|
- `platform`: Must be `NuHeat`
|
|
62
64
|
- `name`: Display name used in Homebridge logs
|
|
63
65
|
- `email`: MyNuheat account email address
|
|
66
|
+
- `Email`: Legacy alias still accepted for backward compatibility, but `email` is the preferred documented field
|
|
64
67
|
- `password`: MyNuheat account password
|
|
65
|
-
- `devices`: Optional list of thermostats to expose
|
|
68
|
+
- `devices`: Optional list of thermostats to expose. If omitted or empty, every thermostat on the account will be discovered automatically
|
|
66
69
|
- `serialNumber`: Thermostat serial number from MyNuheat
|
|
67
|
-
- `autoPopulateAwayModeSwitches`: Automatically expose switches for all groups on the account
|
|
68
|
-
- `
|
|
70
|
+
- `autoPopulateAwayModeSwitches`: Automatically expose away-mode switches for all groups on the account
|
|
71
|
+
- `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
|
|
72
|
+
- `groups`: Optional allow-list of groups to expose as away-mode switches. This only affects group/away-mode accessories
|
|
69
73
|
- `groupName`: Group name as shown in MyNuheat
|
|
70
74
|
- `holdLength`: Hold duration in minutes
|
|
71
75
|
- `refresh`: Poll interval in seconds, default `60`
|
|
72
76
|
- `debug`: Enables verbose logging
|
|
73
|
-
- `clientId`: Optional advanced override for the Nuheat OAuth client ID
|
|
77
|
+
- `clientId`: Optional advanced override for the Nuheat OAuth client ID. This is recommended once you have official Nuheat API credentials
|
|
74
78
|
- `clientSecret`: Optional advanced override for the Nuheat OAuth client secret
|
|
75
79
|
- `redirectUri`: Optional advanced override for the Nuheat OAuth redirect URI, default `http://localhost`
|
|
76
80
|
|
|
@@ -86,6 +90,8 @@ If `devices` is omitted or empty, the plugin will automatically expose every the
|
|
|
86
90
|
|
|
87
91
|
If `groups` is omitted and `autoPopulateAwayModeSwitches` is enabled, the plugin will automatically expose away-mode switches for all groups on the account.
|
|
88
92
|
|
|
93
|
+
If `exposeScheduleSwitches` is enabled, the plugin will also create one switch per thermostat that turns on when the thermostat is following its Nuheat schedule and can be used to resume that schedule from HomeKit.
|
|
94
|
+
|
|
89
95
|
## Nuheat API Access
|
|
90
96
|
|
|
91
97
|
Nuheat's public OpenAPI documentation indicates that third-party developers should request their own API credentials:
|
|
@@ -93,7 +99,7 @@ Nuheat's public OpenAPI documentation indicates that third-party developers shou
|
|
|
93
99
|
- [Nuheat OpenAPI docs](https://api.mynuheat.com/)
|
|
94
100
|
- [Nuheat API access request page](https://www.nuheat.com/openapi)
|
|
95
101
|
|
|
96
|
-
This fork still supports the legacy built-in OAuth client settings as a fallback, but using your own `clientId` and `clientSecret` is the recommended long-term path.
|
|
102
|
+
This fork still supports the legacy built-in OAuth client settings as a fallback, but using your own `clientId` and `clientSecret` is the recommended long-term path once Nuheat issues them for your integration.
|
|
97
103
|
|
|
98
104
|
## What's New In This Fork
|
|
99
105
|
|
|
@@ -104,6 +110,7 @@ This fork still supports the legacy built-in OAuth client settings as a fallback
|
|
|
104
110
|
- Added regression tests for the key thermostat behavior fixes
|
|
105
111
|
- Updated package metadata and dependency overrides for a cleaner modern release
|
|
106
112
|
- Published under the maintainer-owned package identity `homebridge-nuheat2`
|
|
113
|
+
- Added Swagger-aligned account, schedule, and energy API helpers for future enhancements
|
|
107
114
|
|
|
108
115
|
## Development
|
|
109
116
|
|
package/config.schema.json
CHANGED
|
@@ -60,6 +60,11 @@
|
|
|
60
60
|
"title": "Auto populate Away Mode switches for all available groups",
|
|
61
61
|
"type": "boolean"
|
|
62
62
|
},
|
|
63
|
+
"exposeScheduleSwitches": {
|
|
64
|
+
"title": "Expose per-thermostat schedule switches",
|
|
65
|
+
"type": "boolean",
|
|
66
|
+
"description": "Creates a switch for each thermostat that reflects whether its schedule is active and can be turned on to resume the configured schedule."
|
|
67
|
+
},
|
|
63
68
|
"groups": {
|
|
64
69
|
"title": "Groups",
|
|
65
70
|
"type": "array",
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
let NuHeatAPI = require("./lib/NuHeatAPI.js");
|
|
4
4
|
let NuHeatGroup = require("./lib/NuHeatGroup.js");
|
|
5
|
+
let NuHeatScheduleSwitch = require("./lib/NuHeatScheduleSwitch.js");
|
|
5
6
|
let NuHeatThermostat = require("./lib/NuHeatThermostat.js");
|
|
6
7
|
let NuHeatListener = require("./lib/NuHeatListener.js");
|
|
7
8
|
const logger = require("./lib/logger");
|
|
@@ -88,6 +89,7 @@ class NuHeatPlatform {
|
|
|
88
89
|
);
|
|
89
90
|
|
|
90
91
|
if (await this.NuHeatAPI.returnAccessToken()) {
|
|
92
|
+
await this.loadAccount();
|
|
91
93
|
await this.setupGroups();
|
|
92
94
|
await this.setupThermostats();
|
|
93
95
|
this.cleanupRemovedAccessories();
|
|
@@ -270,10 +272,58 @@ class NuHeatPlatform {
|
|
|
270
272
|
this.accessories
|
|
271
273
|
.find((accessory) => accessory.uuid === uuid)
|
|
272
274
|
.accessory.updateValues(deviceData);
|
|
275
|
+
|
|
276
|
+
if (this.config.exposeScheduleSwitches) {
|
|
277
|
+
this.setupScheduleSwitch(deviceData);
|
|
278
|
+
}
|
|
273
279
|
}),
|
|
274
280
|
);
|
|
275
281
|
}
|
|
276
282
|
|
|
283
|
+
setupScheduleSwitch(deviceData) {
|
|
284
|
+
const uuid = UUIDGen.generate(
|
|
285
|
+
deviceData.serialNumber.toString() + "-schedule",
|
|
286
|
+
);
|
|
287
|
+
let deviceAccessory = false;
|
|
288
|
+
|
|
289
|
+
if (this.accessories.find((accessory) => accessory.uuid === uuid)) {
|
|
290
|
+
deviceAccessory = this.accessories.find(
|
|
291
|
+
(accessory) => accessory.uuid === uuid,
|
|
292
|
+
).accessory;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!deviceAccessory) {
|
|
296
|
+
this.log.info("Creating schedule switch for thermostat", deviceData.name);
|
|
297
|
+
const accessory = new PlatformAccessory(
|
|
298
|
+
deviceData.name + " Schedule",
|
|
299
|
+
uuid,
|
|
300
|
+
);
|
|
301
|
+
accessory.addService(Service.Switch, deviceData.name + " Schedule");
|
|
302
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
|
|
303
|
+
accessory,
|
|
304
|
+
]);
|
|
305
|
+
deviceAccessory = accessory;
|
|
306
|
+
this.accessories.push({ uuid });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.accessories.find((accessory) => accessory.uuid === uuid).accessory =
|
|
310
|
+
new NuHeatScheduleSwitch(
|
|
311
|
+
this.log,
|
|
312
|
+
deviceData,
|
|
313
|
+
deviceAccessory instanceof NuHeatScheduleSwitch
|
|
314
|
+
? deviceAccessory.accessory
|
|
315
|
+
: deviceAccessory,
|
|
316
|
+
this.NuHeatAPI,
|
|
317
|
+
Homebridge,
|
|
318
|
+
);
|
|
319
|
+
this.accessories.find(
|
|
320
|
+
(accessory) => accessory.uuid === uuid,
|
|
321
|
+
).existsInConfig = true;
|
|
322
|
+
this.accessories
|
|
323
|
+
.find((accessory) => accessory.uuid === uuid)
|
|
324
|
+
.accessory.updateValues(deviceData);
|
|
325
|
+
}
|
|
326
|
+
|
|
277
327
|
cleanupRemovedAccessories() {
|
|
278
328
|
this.accessories.forEach(function (thisAccessory) {
|
|
279
329
|
if (thisAccessory.existsInConfig !== true) {
|
|
@@ -341,11 +391,35 @@ class NuHeatPlatform {
|
|
|
341
391
|
if (thisAccessory) {
|
|
342
392
|
thisAccessory.accessory.updateValues(deviceData);
|
|
343
393
|
}
|
|
394
|
+
|
|
395
|
+
const scheduleAccessory = this.accessories.find(
|
|
396
|
+
(accessory) =>
|
|
397
|
+
accessory.uuid ===
|
|
398
|
+
UUIDGen.generate(deviceData.serialNumber.toString() + "-schedule"),
|
|
399
|
+
);
|
|
400
|
+
if (scheduleAccessory) {
|
|
401
|
+
scheduleAccessory.accessory.updateValues(deviceData);
|
|
402
|
+
}
|
|
344
403
|
}, this);
|
|
345
404
|
|
|
346
405
|
return true;
|
|
347
406
|
}
|
|
348
407
|
|
|
408
|
+
async loadAccount() {
|
|
409
|
+
const account = await this.NuHeatAPI.getAccount();
|
|
410
|
+
if (!account) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.account = account;
|
|
415
|
+
this.log.debug(
|
|
416
|
+
"NuHeat account preferences loaded. Temperature scale: " +
|
|
417
|
+
(account.temperatureScale || "unknown") +
|
|
418
|
+
", 12-hour clock: " +
|
|
419
|
+
String(account.use12Hour),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
349
423
|
teardown() {
|
|
350
424
|
if (this.refreshTimer) {
|
|
351
425
|
clearInterval(this.refreshTimer);
|
package/lib/NuHeatAPI.js
CHANGED
|
@@ -18,6 +18,14 @@ const {
|
|
|
18
18
|
NUHEAT_API_TOKEN_URI,
|
|
19
19
|
NUHEAT_API_CONSENT_URI,
|
|
20
20
|
} = require("./settings");
|
|
21
|
+
const {
|
|
22
|
+
SCHEDULE_MODE,
|
|
23
|
+
normalizeAccount,
|
|
24
|
+
normalizeEnergyUsage,
|
|
25
|
+
normalizeGroup,
|
|
26
|
+
normalizeSchedule,
|
|
27
|
+
normalizeThermostat,
|
|
28
|
+
} = require("./NuHeatModels");
|
|
21
29
|
|
|
22
30
|
module.exports = class NuHeatAPI {
|
|
23
31
|
constructor(email, password, log, options = {}) {
|
|
@@ -108,12 +116,37 @@ module.exports = class NuHeatAPI {
|
|
|
108
116
|
return returnedData;
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
async resumeSchedule(serialNumber) {
|
|
120
|
+
return this.updateThermostat({
|
|
121
|
+
serialNumber,
|
|
122
|
+
scheduleMode: SCHEDULE_MODE.AUTO,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async updateThermostat(thermostatUpdate) {
|
|
127
|
+
const callURL = "https://api.mynuheat.com/api/v1/Thermostat";
|
|
128
|
+
const callOptions = {
|
|
129
|
+
body: JSON.stringify(thermostatUpdate),
|
|
130
|
+
method: "PUT",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return await this.makeAPICall(callURL, callOptions, {
|
|
134
|
+
normalize: normalizeThermostat,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
111
138
|
// get data for a group
|
|
112
139
|
async refreshGroup(groupId) {
|
|
113
140
|
// set the URL for the call
|
|
114
141
|
const callURL = "https://api.mynuheat.com/api/v1/Group/" + groupId;
|
|
115
142
|
|
|
116
|
-
let returnedData = await this.makeAPICall(
|
|
143
|
+
let returnedData = await this.makeAPICall(
|
|
144
|
+
callURL,
|
|
145
|
+
{},
|
|
146
|
+
{
|
|
147
|
+
normalize: normalizeGroup,
|
|
148
|
+
},
|
|
149
|
+
);
|
|
117
150
|
return returnedData;
|
|
118
151
|
}
|
|
119
152
|
|
|
@@ -122,7 +155,14 @@ module.exports = class NuHeatAPI {
|
|
|
122
155
|
// set the URL for the call
|
|
123
156
|
const callURL = "https://api.mynuheat.com/api/v1/Group";
|
|
124
157
|
|
|
125
|
-
let returnedData = await this.makeAPICall(
|
|
158
|
+
let returnedData = await this.makeAPICall(
|
|
159
|
+
callURL,
|
|
160
|
+
{},
|
|
161
|
+
{
|
|
162
|
+
normalize: normalizeGroup,
|
|
163
|
+
normalizeArray: true,
|
|
164
|
+
},
|
|
165
|
+
);
|
|
126
166
|
return returnedData;
|
|
127
167
|
}
|
|
128
168
|
|
|
@@ -132,7 +172,13 @@ module.exports = class NuHeatAPI {
|
|
|
132
172
|
const callURL =
|
|
133
173
|
"https://api.mynuheat.com/api/v1/Thermostat/" + serialNumber;
|
|
134
174
|
|
|
135
|
-
let returnedData = await this.makeAPICall(
|
|
175
|
+
let returnedData = await this.makeAPICall(
|
|
176
|
+
callURL,
|
|
177
|
+
{},
|
|
178
|
+
{
|
|
179
|
+
normalize: normalizeThermostat,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
136
182
|
return returnedData;
|
|
137
183
|
}
|
|
138
184
|
|
|
@@ -141,11 +187,111 @@ module.exports = class NuHeatAPI {
|
|
|
141
187
|
// set the URL for the call
|
|
142
188
|
const callURL = "https://api.mynuheat.com/api/v1/Thermostat";
|
|
143
189
|
|
|
144
|
-
let returnedData = await this.makeAPICall(
|
|
190
|
+
let returnedData = await this.makeAPICall(
|
|
191
|
+
callURL,
|
|
192
|
+
{},
|
|
193
|
+
{
|
|
194
|
+
normalize: normalizeThermostat,
|
|
195
|
+
normalizeArray: true,
|
|
196
|
+
},
|
|
197
|
+
);
|
|
145
198
|
return returnedData;
|
|
146
199
|
}
|
|
147
200
|
|
|
148
|
-
async
|
|
201
|
+
async getAccount() {
|
|
202
|
+
const callURL = "https://api.mynuheat.com/api/v1/Account";
|
|
203
|
+
return await this.makeAPICall(
|
|
204
|
+
callURL,
|
|
205
|
+
{},
|
|
206
|
+
{
|
|
207
|
+
normalize: normalizeAccount,
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async refreshSchedule(serialNumber) {
|
|
213
|
+
const callURL = "https://api.mynuheat.com/api/v1/Schedule/" + serialNumber;
|
|
214
|
+
return await this.makeAPICall(
|
|
215
|
+
callURL,
|
|
216
|
+
{},
|
|
217
|
+
{
|
|
218
|
+
normalize: normalizeSchedule,
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async refreshSchedules() {
|
|
224
|
+
const callURL = "https://api.mynuheat.com/api/v1/Schedule";
|
|
225
|
+
return await this.makeAPICall(
|
|
226
|
+
callURL,
|
|
227
|
+
{},
|
|
228
|
+
{
|
|
229
|
+
normalize: normalizeSchedule,
|
|
230
|
+
normalizeArray: true,
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async updateSchedule(scheduleModel) {
|
|
236
|
+
const callURL = "https://api.mynuheat.com/api/v1/Schedule";
|
|
237
|
+
return await this.makeAPICall(
|
|
238
|
+
callURL,
|
|
239
|
+
{
|
|
240
|
+
body: JSON.stringify(scheduleModel),
|
|
241
|
+
method: "PUT",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
treatNoContentAsSuccess: true,
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async refreshEnergyLogDay(serialNumber, date) {
|
|
250
|
+
const callURL =
|
|
251
|
+
"https://api.mynuheat.com/api/v1/EnergyLog/Day/" +
|
|
252
|
+
serialNumber +
|
|
253
|
+
"/" +
|
|
254
|
+
date;
|
|
255
|
+
return await this.makeAPICall(
|
|
256
|
+
callURL,
|
|
257
|
+
{},
|
|
258
|
+
{
|
|
259
|
+
normalize: normalizeEnergyUsage,
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async refreshEnergyLogWeek(serialNumber, date) {
|
|
265
|
+
const callURL =
|
|
266
|
+
"https://api.mynuheat.com/api/v1/EnergyLog/Week/" +
|
|
267
|
+
serialNumber +
|
|
268
|
+
"/" +
|
|
269
|
+
date;
|
|
270
|
+
return await this.makeAPICall(
|
|
271
|
+
callURL,
|
|
272
|
+
{},
|
|
273
|
+
{
|
|
274
|
+
normalize: normalizeEnergyUsage,
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async refreshEnergyLogMonth(serialNumber, year) {
|
|
280
|
+
const callURL =
|
|
281
|
+
"https://api.mynuheat.com/api/v1/EnergyLog/Month/" +
|
|
282
|
+
serialNumber +
|
|
283
|
+
"/" +
|
|
284
|
+
year;
|
|
285
|
+
return await this.makeAPICall(
|
|
286
|
+
callURL,
|
|
287
|
+
{},
|
|
288
|
+
{
|
|
289
|
+
normalize: normalizeEnergyUsage,
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async makeAPICall(callURL, callOptions = {}, options = {}) {
|
|
149
295
|
// Validate and potentially refresh our access token.
|
|
150
296
|
if (!(await this.refreshAccessToken())) {
|
|
151
297
|
return false;
|
|
@@ -161,9 +307,21 @@ module.exports = class NuHeatAPI {
|
|
|
161
307
|
}
|
|
162
308
|
//handle PUT successes that don't return a body
|
|
163
309
|
if (response.status === 204) {
|
|
310
|
+
if (options.treatNoContentAsSuccess) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
164
313
|
return true;
|
|
165
314
|
}
|
|
166
315
|
let returnedData = await response.json();
|
|
316
|
+
|
|
317
|
+
if (options.normalize) {
|
|
318
|
+
if (options.normalizeArray && Array.isArray(returnedData)) {
|
|
319
|
+
return returnedData.map(options.normalize);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return options.normalize(returnedData);
|
|
323
|
+
}
|
|
324
|
+
|
|
167
325
|
return returnedData;
|
|
168
326
|
}
|
|
169
327
|
|
package/lib/NuHeatListener.js
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const SCHEDULE_MODE = {
|
|
4
|
+
AUTO: 1,
|
|
5
|
+
HOLD: 2,
|
|
6
|
+
PERMANENT_HOLD: 3,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const OPERATING_MODE = {
|
|
10
|
+
AUTO: 1,
|
|
11
|
+
MANUAL: 2,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function coalesce(...values) {
|
|
15
|
+
return values.find((value) => value !== undefined && value !== null);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeAccount(account = {}) {
|
|
19
|
+
return {
|
|
20
|
+
firstName: coalesce(account.firstName, account.FirstName),
|
|
21
|
+
lastName: coalesce(account.lastName, account.LastName),
|
|
22
|
+
email: coalesce(account.email, account.Email),
|
|
23
|
+
use12Hour: coalesce(account.use12Hour, account.Use12Hour),
|
|
24
|
+
temperatureScale: coalesce(
|
|
25
|
+
account.temperatureScale,
|
|
26
|
+
account.TemperatureScale,
|
|
27
|
+
),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeGroup(group = {}) {
|
|
32
|
+
return {
|
|
33
|
+
groupId: coalesce(group.groupId, group.GroupId),
|
|
34
|
+
groupName: coalesce(group.groupName, group.GroupName),
|
|
35
|
+
awayMode: coalesce(group.awayMode, group.AwayMode),
|
|
36
|
+
awaySetPointTemp: coalesce(group.awaySetPointTemp, group.AwaySetPointTemp),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeScheduleEvent(event = {}) {
|
|
41
|
+
return {
|
|
42
|
+
clock: coalesce(event.clock, event.Clock),
|
|
43
|
+
scheduleType: coalesce(event.scheduleType, event.ScheduleType),
|
|
44
|
+
active: coalesce(event.active, event.Active),
|
|
45
|
+
temperature: coalesce(event.temperature, event.Temperature),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeScheduleDay(day = {}) {
|
|
50
|
+
return {
|
|
51
|
+
weekDay: coalesce(day.weekDay, day.WeekDay),
|
|
52
|
+
weekDayGroupNumber: coalesce(
|
|
53
|
+
day.weekDayGroupNumber,
|
|
54
|
+
day.WeekDayGroupNumber,
|
|
55
|
+
),
|
|
56
|
+
events: (coalesce(day.events, day.Events) || []).map(
|
|
57
|
+
normalizeScheduleEvent,
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeSchedule(schedule = {}) {
|
|
63
|
+
return {
|
|
64
|
+
serialNumber: coalesce(schedule.serialNumber, schedule.SerialNumber),
|
|
65
|
+
days: (coalesce(schedule.days, schedule.Days) || []).map(
|
|
66
|
+
normalizeScheduleDay,
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeThermostat(thermostat = {}) {
|
|
72
|
+
return {
|
|
73
|
+
serialNumber: coalesce(thermostat.serialNumber, thermostat.SerialNumber),
|
|
74
|
+
name: coalesce(thermostat.name, thermostat.Name),
|
|
75
|
+
setPointTemp: coalesce(thermostat.setPointTemp, thermostat.SetPointTemp),
|
|
76
|
+
scheduleMode: coalesce(thermostat.scheduleMode, thermostat.ScheduleMode),
|
|
77
|
+
holdSetPointDateTime: coalesce(
|
|
78
|
+
thermostat.holdSetPointDateTime,
|
|
79
|
+
thermostat.HoldSetPointDateTime,
|
|
80
|
+
),
|
|
81
|
+
groupId: coalesce(thermostat.groupId, thermostat.GroupId),
|
|
82
|
+
swVersion: coalesce(thermostat.swVersion, thermostat.SwVersion),
|
|
83
|
+
online: coalesce(thermostat.online, thermostat.Online),
|
|
84
|
+
operatingMode: coalesce(thermostat.operatingMode, thermostat.OperatingMode),
|
|
85
|
+
isHeating: coalesce(thermostat.isHeating, thermostat.IsHeating),
|
|
86
|
+
currentTemperature: coalesce(
|
|
87
|
+
thermostat.currentTemperature,
|
|
88
|
+
thermostat.CurrentTemperature,
|
|
89
|
+
),
|
|
90
|
+
tzOffset: coalesce(thermostat.tzOffset, thermostat.TZOffset),
|
|
91
|
+
error: coalesce(thermostat.error, thermostat.Error),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeEnergyUsageEntry(entry = {}) {
|
|
96
|
+
return {
|
|
97
|
+
entry: coalesce(entry.entry, entry.Entry),
|
|
98
|
+
minutes: coalesce(entry.minutes, entry.Minutes),
|
|
99
|
+
energyKWattHour: coalesce(entry.energyKWattHour, entry.EnergyKWattHour),
|
|
100
|
+
chargeKWattHour: coalesce(entry.chargeKWattHour, entry.ChargeKWattHour),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeEnergyUsage(energyUsage = {}) {
|
|
105
|
+
return {
|
|
106
|
+
energyUsageType: coalesce(
|
|
107
|
+
energyUsage.energyUsageType,
|
|
108
|
+
energyUsage.EnergyUsageType,
|
|
109
|
+
),
|
|
110
|
+
energyUsageFrom: coalesce(
|
|
111
|
+
energyUsage.energyUsageFrom,
|
|
112
|
+
energyUsage.EnergyUsageFrom,
|
|
113
|
+
),
|
|
114
|
+
energyUsageTo: coalesce(
|
|
115
|
+
energyUsage.energyUsageTo,
|
|
116
|
+
energyUsage.EnergyUsageTo,
|
|
117
|
+
),
|
|
118
|
+
mondayIsFirstDay: coalesce(
|
|
119
|
+
energyUsage.mondayIsFirstDay,
|
|
120
|
+
energyUsage.MondayIsFirstDay,
|
|
121
|
+
),
|
|
122
|
+
energyUsage: (
|
|
123
|
+
coalesce(energyUsage.energyUsage, energyUsage.EnergyUsage) || []
|
|
124
|
+
).map(normalizeEnergyUsageEntry),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
OPERATING_MODE,
|
|
130
|
+
SCHEDULE_MODE,
|
|
131
|
+
normalizeAccount,
|
|
132
|
+
normalizeEnergyUsage,
|
|
133
|
+
normalizeGroup,
|
|
134
|
+
normalizeSchedule,
|
|
135
|
+
normalizeThermostat,
|
|
136
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
let Characteristic, SwitchService;
|
|
4
|
+
|
|
5
|
+
const { SCHEDULE_MODE } = require("./NuHeatModels");
|
|
6
|
+
|
|
7
|
+
module.exports = class NuHeatScheduleSwitch {
|
|
8
|
+
constructor(log, deviceData, accessory, nuHeatAPI, homebridge) {
|
|
9
|
+
Characteristic = homebridge.hap.Characteristic;
|
|
10
|
+
SwitchService = homebridge.hap.Service.Switch;
|
|
11
|
+
this.log = log;
|
|
12
|
+
this.deviceData = deviceData;
|
|
13
|
+
this.accessory = accessory;
|
|
14
|
+
this.nuHeatAPI = nuHeatAPI;
|
|
15
|
+
|
|
16
|
+
this.accessory
|
|
17
|
+
.getService(homebridge.hap.Service.AccessoryInformation)
|
|
18
|
+
.setCharacteristic(Characteristic.Manufacturer, "NuHeat")
|
|
19
|
+
.setCharacteristic(Characteristic.Model, "Signature Schedule")
|
|
20
|
+
.setCharacteristic(
|
|
21
|
+
Characteristic.SerialNumber,
|
|
22
|
+
this.deviceData.serialNumber + "-schedule",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
this.setupListeners();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setupListeners() {
|
|
29
|
+
this.accessory
|
|
30
|
+
.getService(SwitchService)
|
|
31
|
+
.getCharacteristic(Characteristic.On)
|
|
32
|
+
.on("set", this.setScheduleEnabled.bind(this));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async setScheduleEnabled(value, callback) {
|
|
36
|
+
if (!value) {
|
|
37
|
+
callback(null);
|
|
38
|
+
this.updateValues(this.deviceData);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.log.debug(
|
|
43
|
+
"Resuming schedule for thermostat " + this.deviceData.serialNumber,
|
|
44
|
+
this.deviceData.name,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const response = await this.nuHeatAPI.resumeSchedule(
|
|
48
|
+
this.deviceData.serialNumber,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!response) {
|
|
52
|
+
this.log.error("Error resuming schedule", this.deviceData.name);
|
|
53
|
+
callback(new Error("Error: resumeSchedule"));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.updateValues(response);
|
|
58
|
+
callback(null);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
updateValues(newValues) {
|
|
62
|
+
this.deviceData = newValues;
|
|
63
|
+
const scheduleEnabled = newValues.scheduleMode === SCHEDULE_MODE.AUTO;
|
|
64
|
+
this.accessory
|
|
65
|
+
.getService(SwitchService)
|
|
66
|
+
.getCharacteristic(Characteristic.On)
|
|
67
|
+
.updateValue(scheduleEnabled);
|
|
68
|
+
}
|
|
69
|
+
};
|
package/lib/NuHeatThermostat.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { OPERATING_MODE, SCHEDULE_MODE } = require("./NuHeatModels");
|
|
2
|
+
|
|
1
3
|
let Characteristic, ThermostatService;
|
|
2
4
|
module.exports = class NuHeatThermostat {
|
|
3
5
|
constructor(log, deviceData, holdLength, accessory, NuHeatAPI, homebridge) {
|
|
@@ -52,7 +54,7 @@ module.exports = class NuHeatThermostat {
|
|
|
52
54
|
.on("set", this.setTargetTemperature.bind(this));
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
//
|
|
57
|
+
// Nuheat exposes heat-only thermostats, so this characteristic is informational.
|
|
56
58
|
setTargetHeatingCooling(value, callback) {
|
|
57
59
|
callback(null);
|
|
58
60
|
this.updateAccessory();
|
|
@@ -145,9 +147,8 @@ module.exports = class NuHeatThermostat {
|
|
|
145
147
|
.updateValue(CurrentHeatingCoolingState);
|
|
146
148
|
|
|
147
149
|
// system switch mode
|
|
148
|
-
var TargetHeatingCooling =
|
|
149
|
-
newValues
|
|
150
|
-
);
|
|
150
|
+
var TargetHeatingCooling =
|
|
151
|
+
this.toHomeBridgeHeatingCoolingState(newValues);
|
|
151
152
|
this.log.debug(
|
|
152
153
|
"Target heating state is " + TargetHeatingCooling,
|
|
153
154
|
this.deviceData.name,
|
|
@@ -176,55 +177,23 @@ module.exports = class NuHeatThermostat {
|
|
|
176
177
|
return ((((temperature - 33) / 56 + 33 - 32) * 5) / 9).toFixed(1);
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
toHomeBridgeHeatingCoolingState(newValues) {
|
|
181
|
+
if (!this.isOnline(newValues)) {
|
|
182
|
+
return Characteristic.TargetHeatingCoolingState.OFF;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
switch (newValues.operatingMode) {
|
|
186
|
+
case OPERATING_MODE.AUTO:
|
|
187
|
+
case OPERATING_MODE.MANUAL:
|
|
186
188
|
return Characteristic.TargetHeatingCoolingState.HEAT;
|
|
187
|
-
break;
|
|
188
|
-
case 3:
|
|
189
|
-
// cool
|
|
190
|
-
case 7:
|
|
191
|
-
// "Drying" (MHK1)
|
|
192
|
-
return Characteristic.TargetHeatingCoolingState.COOL;
|
|
193
|
-
break;
|
|
194
|
-
case 4:
|
|
195
|
-
// autoheat
|
|
196
|
-
case 5:
|
|
197
|
-
// autocool
|
|
198
|
-
return Characteristic.TargetHeatingCoolingState.AUTO;
|
|
199
|
-
break;
|
|
200
|
-
case 6:
|
|
201
|
-
// "Southern Away" humidity control
|
|
202
189
|
default:
|
|
203
|
-
|
|
190
|
+
// The Swagger spec currently documents only AUTO=1 and MANUAL=2.
|
|
191
|
+
return Characteristic.TargetHeatingCoolingState.HEAT;
|
|
204
192
|
}
|
|
205
193
|
}
|
|
206
194
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
case Characteristic.TargetHeatingCoolingState.OFF:
|
|
210
|
-
// off
|
|
211
|
-
return 2;
|
|
212
|
-
break;
|
|
213
|
-
case Characteristic.TargetHeatingCoolingState.HEAT:
|
|
214
|
-
// heat
|
|
215
|
-
return 1;
|
|
216
|
-
break;
|
|
217
|
-
case Characteristic.TargetHeatingCoolingState.COOL:
|
|
218
|
-
// cool
|
|
219
|
-
return 3;
|
|
220
|
-
break;
|
|
221
|
-
case Characteristic.TargetHeatingCoolingState.AUTO:
|
|
222
|
-
// auto
|
|
223
|
-
return 4;
|
|
224
|
-
break;
|
|
225
|
-
default:
|
|
226
|
-
return 0;
|
|
227
|
-
}
|
|
195
|
+
isScheduleEnabled(newValues) {
|
|
196
|
+
return newValues.scheduleMode === SCHEDULE_MODE.AUTO;
|
|
228
197
|
}
|
|
229
198
|
|
|
230
199
|
isOnline(newValues) {
|