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 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-beta.0] - 2026-04-11
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 basic regression tests and modernize package metadata for Homebridge 1.8+ and 2.0 betas
15
- - Publish the maintained fork under the new npm package identity `homebridge-nuheat2`
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
- - `groups`: Optional allow-list of groups to expose as away-mode switches
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
 
@@ -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(callURL);
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(callURL);
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(callURL);
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(callURL);
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 makeAPICall(callURL, callOptions = {}) {
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
 
@@ -112,6 +112,7 @@ module.exports = class NuHeatListener {
112
112
  break;
113
113
  case 3:
114
114
  notificationType = "Schedule";
115
+ this.nuHeatPlatform.refreshThermostats();
115
116
  break;
116
117
  case 4:
117
118
  notificationType = "Group";
@@ -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
+ };
@@ -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
- // This is to change the system switch to a different mode - that doesn't work for us, its always in heat
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 = this.toHomeBridgeisHeatingCoolingSystem(
149
- newValues.operatingMode,
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
- toHomeBridgeisHeatingCoolingSystem(isHeatingCoolingSystem) {
180
- switch (isHeatingCoolingSystem) {
181
- case 0:
182
- // emergency heat
183
- case 1:
184
- case 2:
185
- // heat, including manual mode
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
- return Characteristic.TargetHeatingCoolingState.OFF;
190
+ // The Swagger spec currently documents only AUTO=1 and MANUAL=2.
191
+ return Characteristic.TargetHeatingCoolingState.HEAT;
204
192
  }
205
193
  }
206
194
 
207
- toNuHeatisHeatingCoolingSystem(isHeatingCoolingSystem) {
208
- switch (isHeatingCoolingSystem) {
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-nuheat2",
3
- "version": "1.2.4-beta.0",
3
+ "version": "1.2.4",
4
4
  "description": "Homebridge Platform for NuHeat Signature Thermostats",
5
5
  "main": "index.js",
6
6
  "scripts": {