homebridge-nuheat2 1.2.11 → 1.2.12

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,15 @@ All notable changes to this project should be documented in this file
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.12] - 2026-04-21
8
+
9
+ ### Fixed
10
+
11
+ - Treat Nuheat thermostats as heat-only in HomeKit so the target mode no longer advertises a misleading off option
12
+ - Translate any incoming HomeKit off request into the minimum Nuheat setpoint instead of bouncing back after refresh
13
+ - Apply the Nuheat account temperature scale to the thermostat display-units characteristic so HomeKit shows the correct hardware display unit
14
+ - Coalesce and briefly de-duplicate repeated SignalR thermostat and group notifications to reduce redundant full refreshes and log noise
15
+
7
16
  ## [1.2.11] - 2026-04-21
8
17
 
9
18
  ### Fixed
package/index.js CHANGED
@@ -189,7 +189,7 @@ class NuHeatPlatform {
189
189
  entry = { uuid };
190
190
  this.accessories.push(entry);
191
191
  }
192
- entry.accessory = new NuHeatThermostat(this.log, deviceData, this.config.holdLength || 1440, getPlatformAccessory(deviceAccessory), this.NuHeatAPI, Homebridge);
192
+ entry.accessory = new NuHeatThermostat(this.log, deviceData, this.config.holdLength || 1440, getPlatformAccessory(deviceAccessory), this.NuHeatAPI, Homebridge, this.account?.temperatureScale);
193
193
  entry.existsInConfig = true;
194
194
  this.log.info("Loaded thermostat " +
195
195
  deviceData.serialNumber +
@@ -1,16 +1,19 @@
1
1
  "use strict";
2
2
  const signalR = require("@microsoft/signalr");
3
+ const NOTIFICATION_DEDUPE_WINDOW_MS = 2000;
3
4
  class NuHeatListener {
4
5
  nuHeatAPI;
5
6
  nuHeatPlatform;
6
7
  log;
7
8
  notificationTypes;
8
9
  connection;
10
+ recentNotifications;
9
11
  constructor(nuHeatAPI, nuheatPlatform) {
10
12
  this.nuHeatAPI = nuHeatAPI;
11
13
  this.nuHeatPlatform = nuheatPlatform;
12
14
  this.log = nuheatPlatform.log;
13
15
  this.notificationTypes = ["2", "4"];
16
+ this.recentNotifications = new Map();
14
17
  this.connection = new signalR.HubConnectionBuilder()
15
18
  .withUrl("https://api.mynuheat.com/notificationsHost", {
16
19
  accessTokenFactory: async () => {
@@ -73,8 +76,10 @@ class NuHeatListener {
73
76
  });
74
77
  }
75
78
  traceNotification(notificationList) {
79
+ let shouldRefreshThermostats = false;
80
+ let shouldRefreshGroups = false;
76
81
  notificationList.forEach((notification) => {
77
- let notificationType = "";
82
+ let notificationType = "Unknown";
78
83
  switch (notification.type) {
79
84
  case 0:
80
85
  case 1:
@@ -82,15 +87,15 @@ class NuHeatListener {
82
87
  break;
83
88
  case 2:
84
89
  notificationType = "Thermostat";
85
- void this.nuHeatPlatform.refreshThermostats();
90
+ shouldRefreshThermostats = shouldRefreshThermostats || !this.isDuplicateNotification(notification);
86
91
  break;
87
92
  case 3:
88
93
  notificationType = "Schedule";
89
- void this.nuHeatPlatform.refreshThermostats();
94
+ shouldRefreshThermostats = shouldRefreshThermostats || !this.isDuplicateNotification(notification);
90
95
  break;
91
96
  case 4:
92
97
  notificationType = "Group";
93
- void this.nuHeatPlatform.refreshGroups();
98
+ shouldRefreshGroups = shouldRefreshGroups || !this.isDuplicateNotification(notification);
94
99
  break;
95
100
  }
96
101
  this.log.debug(notificationType +
@@ -98,10 +103,38 @@ class NuHeatListener {
98
103
  notification.id +
99
104
  " at " +
100
105
  notification.timeStamp +
101
- ". Refreshing data for all " +
102
- notificationType +
103
- "s");
106
+ ".");
104
107
  });
108
+ if (shouldRefreshThermostats) {
109
+ void this.nuHeatPlatform.refreshThermostats();
110
+ }
111
+ if (shouldRefreshGroups) {
112
+ void this.nuHeatPlatform.refreshGroups();
113
+ }
114
+ }
115
+ isDuplicateNotification(notification) {
116
+ const key = String(notification.type) +
117
+ ":" +
118
+ String(notification.id) +
119
+ ":" +
120
+ notification.timeStamp;
121
+ const now = Date.now();
122
+ this.cleanupRecentNotifications(now);
123
+ const lastSeenAt = this.recentNotifications.get(key);
124
+ if (lastSeenAt !== undefined &&
125
+ now - lastSeenAt < NOTIFICATION_DEDUPE_WINDOW_MS) {
126
+ this.log.debug("Ignoring duplicate notification " + key + ".");
127
+ return true;
128
+ }
129
+ this.recentNotifications.set(key, now);
130
+ return false;
131
+ }
132
+ cleanupRecentNotifications(now) {
133
+ for (const [key, seenAt] of this.recentNotifications.entries()) {
134
+ if (now - seenAt >= NOTIFICATION_DEDUPE_WINDOW_MS) {
135
+ this.recentNotifications.delete(key);
136
+ }
137
+ }
105
138
  }
106
139
  }
107
140
  module.exports = NuHeatListener;
@@ -2,13 +2,16 @@
2
2
  const NuHeatModels_1 = require("./NuHeatModels");
3
3
  let Characteristic;
4
4
  let ThermostatService;
5
+ const MIN_TARGET_TEMPERATURE_C = 10;
6
+ const MAX_TARGET_TEMPERATURE_C = 38;
5
7
  class NuHeatThermostat {
6
8
  log;
7
9
  deviceData;
8
10
  holdLength;
9
11
  accessory;
10
12
  NuHeatAPI;
11
- constructor(log, deviceData, holdLength, accessory, NuHeatAPI, homebridge) {
13
+ temperatureScale;
14
+ constructor(log, deviceData, holdLength, accessory, NuHeatAPI, homebridge, temperatureScale) {
12
15
  Characteristic = homebridge.hap.Characteristic;
13
16
  ThermostatService = homebridge.hap.Service.Thermostat;
14
17
  this.log = log;
@@ -16,6 +19,7 @@ class NuHeatThermostat {
16
19
  this.holdLength = holdLength;
17
20
  this.accessory = accessory;
18
21
  this.NuHeatAPI = NuHeatAPI;
22
+ this.temperatureScale = temperatureScale;
19
23
  this.accessory
20
24
  .getService(homebridge.hap.Service.AccessoryInformation)
21
25
  .setCharacteristic(Characteristic.Manufacturer, "NuHeat")
@@ -25,39 +29,48 @@ class NuHeatThermostat {
25
29
  this.setupListeners();
26
30
  }
27
31
  setupListeners() {
32
+ const thermostatService = this.accessory.getService(ThermostatService);
28
33
  this.log.info("holdLength: " + this.holdLength, this.deviceData.name);
29
- this.accessory
30
- .getService(ThermostatService)
34
+ thermostatService
31
35
  .getCharacteristic(Characteristic.TargetHeatingCoolingState)
32
36
  .setProps({
33
- validValues: [0, 1],
37
+ validValues: [Characteristic.TargetHeatingCoolingState.HEAT],
34
38
  })
35
39
  .on("set", this.setTargetHeatingCooling.bind(this));
36
- this.accessory
37
- .getService(ThermostatService)
38
- .getCharacteristic(Characteristic.CurrentTemperature)
39
- .setProps({
40
+ thermostatService.getCharacteristic(Characteristic.CurrentTemperature).setProps({
40
41
  minValue: -100,
41
42
  maxValue: 100,
42
43
  });
43
- this.accessory
44
- .getService(ThermostatService)
44
+ thermostatService
45
45
  .getCharacteristic(Characteristic.TargetTemperature)
46
46
  .setProps({
47
47
  minStep: 0.5,
48
48
  })
49
49
  .on("set", this.setTargetTemperature.bind(this));
50
+ this.updateTemperatureDisplayUnits();
50
51
  }
51
- setTargetHeatingCooling(_value, callback) {
52
+ async setTargetHeatingCooling(value, callback) {
53
+ if (value !== Characteristic.TargetHeatingCoolingState.OFF) {
54
+ callback(null);
55
+ void this.updateAccessory();
56
+ return;
57
+ }
58
+ this.log.info("NuHeat does not support an off mode. Setting the thermostat to its minimum target temperature instead.", this.deviceData.name);
59
+ const response = await this.NuHeatAPI.setHeatSetpoint(this.deviceData.serialNumber ?? "", this.toNuHeatTemperature(MIN_TARGET_TEMPERATURE_C), this.holdLength);
60
+ if (!response) {
61
+ this.log.error("Error setting minimum target temperature for off request", this.deviceData.name);
62
+ callback(new Error("Error: setTargetHeatingCooling"));
63
+ return;
64
+ }
65
+ this.updateValues(response);
52
66
  callback(null);
53
- void this.updateAccessory();
54
67
  }
55
68
  async setTargetTemperature(value, callback) {
56
- this.log.info("Setting target temperature to " + value + "°C", this.deviceData.name);
57
- if (value < 10)
58
- value = 10;
59
- if (value > 38)
60
- value = 38;
69
+ this.log.info("Setting target temperature to " + value + " C", this.deviceData.name);
70
+ if (value < MIN_TARGET_TEMPERATURE_C)
71
+ value = MIN_TARGET_TEMPERATURE_C;
72
+ if (value > MAX_TARGET_TEMPERATURE_C)
73
+ value = MAX_TARGET_TEMPERATURE_C;
61
74
  const heatSetPoint = this.toNuHeatTemperature(value);
62
75
  this.log.debug("setTargetTemperature " + heatSetPoint, this.deviceData.name);
63
76
  const response = await this.NuHeatAPI.setHeatSetpoint(this.deviceData.serialNumber ?? "", heatSetPoint, this.holdLength);
@@ -81,25 +94,29 @@ class NuHeatThermostat {
81
94
  }
82
95
  updateValues(newValues) {
83
96
  if (this.isOnline(newValues)) {
97
+ this.updateTemperatureDisplayUnits();
84
98
  let currentTemperature = Number(this.toHBTemperature(newValues.currentTemperature ?? 0));
85
- this.log.debug("Current temperature is " + currentTemperature + "°C", this.deviceData.name);
99
+ this.log.debug("Current temperature is " + currentTemperature + " C", this.deviceData.name);
86
100
  this.accessory
87
101
  .getService(ThermostatService)
88
102
  .getCharacteristic(Characteristic.CurrentTemperature)
89
103
  .updateValue(currentTemperature);
90
104
  let setPointTemperature = Number(this.toHBTemperature(newValues.setPointTemp ?? 0));
91
- if (setPointTemperature < 10)
92
- setPointTemperature = 10;
93
- if (setPointTemperature > 38)
94
- setPointTemperature = 38;
95
- this.log.debug("Setpoint temperature is " + setPointTemperature + "°C", this.deviceData.name);
105
+ if (setPointTemperature < MIN_TARGET_TEMPERATURE_C) {
106
+ setPointTemperature = MIN_TARGET_TEMPERATURE_C;
107
+ }
108
+ if (setPointTemperature > MAX_TARGET_TEMPERATURE_C) {
109
+ setPointTemperature = MAX_TARGET_TEMPERATURE_C;
110
+ }
111
+ this.log.debug("Setpoint temperature is " + setPointTemperature + " C", this.deviceData.name);
96
112
  this.accessory
97
113
  .getService(ThermostatService)
98
114
  .getCharacteristic(Characteristic.TargetTemperature)
99
115
  .updateValue(setPointTemperature);
100
- let currentHeatingCoolingState = 0;
116
+ let currentHeatingCoolingState = Characteristic.CurrentHeatingCoolingState.OFF;
101
117
  if (newValues.isHeating) {
102
- currentHeatingCoolingState = 1;
118
+ currentHeatingCoolingState =
119
+ Characteristic.CurrentHeatingCoolingState.HEAT;
103
120
  }
104
121
  this.log.debug("Current heating state is " + currentHeatingCoolingState, this.deviceData.name);
105
122
  this.accessory
@@ -153,5 +170,29 @@ class NuHeatThermostat {
153
170
  }
154
171
  return true;
155
172
  }
173
+ updateTemperatureDisplayUnits() {
174
+ const displayUnits = this.toHomeKitDisplayUnits(this.temperatureScale);
175
+ if (displayUnits === undefined ||
176
+ !Characteristic.TemperatureDisplayUnits) {
177
+ return;
178
+ }
179
+ this.accessory
180
+ .getService(ThermostatService)
181
+ .getCharacteristic(Characteristic.TemperatureDisplayUnits)
182
+ .updateValue(displayUnits);
183
+ }
184
+ toHomeKitDisplayUnits(temperatureScale) {
185
+ if (!temperatureScale || !Characteristic.TemperatureDisplayUnits) {
186
+ return undefined;
187
+ }
188
+ const normalizedScale = temperatureScale.trim().toLowerCase();
189
+ if (normalizedScale.startsWith("f")) {
190
+ return Characteristic.TemperatureDisplayUnits.FAHRENHEIT;
191
+ }
192
+ if (normalizedScale.startsWith("c")) {
193
+ return Characteristic.TemperatureDisplayUnits.CELSIUS;
194
+ }
195
+ return undefined;
196
+ }
156
197
  }
157
198
  module.exports = NuHeatThermostat;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-nuheat2",
3
- "version": "1.2.11",
3
+ "version": "1.2.12",
4
4
  "description": "Homebridge Platform for NuHeat Signature Thermostats",
5
5
  "main": "index.js",
6
6
  "scripts": {