homebridge-nest-accfactory 0.2.11 → 0.3.1

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +14 -7
  3. package/config.schema.json +118 -0
  4. package/dist/HomeKitDevice.js +203 -77
  5. package/dist/HomeKitHistory.js +1 -1
  6. package/dist/config.js +207 -0
  7. package/dist/devices.js +118 -0
  8. package/dist/index.js +2 -1
  9. package/dist/nexustalk.js +46 -48
  10. package/dist/{camera.js → plugins/camera.js} +216 -241
  11. package/dist/{doorbell.js → plugins/doorbell.js} +32 -30
  12. package/dist/plugins/floodlight.js +91 -0
  13. package/dist/plugins/heatlink.js +17 -0
  14. package/dist/{protect.js → plugins/protect.js} +26 -43
  15. package/dist/{tempsensor.js → plugins/tempsensor.js} +15 -19
  16. package/dist/{thermostat.js → plugins/thermostat.js} +426 -383
  17. package/dist/{weather.js → plugins/weather.js} +26 -60
  18. package/dist/protobuf/nest/services/apigateway.proto +31 -1
  19. package/dist/protobuf/nest/trait/firmware.proto +207 -89
  20. package/dist/protobuf/nest/trait/hvac.proto +1052 -312
  21. package/dist/protobuf/nest/trait/located.proto +51 -8
  22. package/dist/protobuf/nest/trait/network.proto +366 -36
  23. package/dist/protobuf/nest/trait/occupancy.proto +145 -17
  24. package/dist/protobuf/nest/trait/product/protect.proto +57 -43
  25. package/dist/protobuf/nest/trait/resourcedirectory.proto +8 -0
  26. package/dist/protobuf/nest/trait/sensor.proto +7 -1
  27. package/dist/protobuf/nest/trait/service.proto +3 -1
  28. package/dist/protobuf/nest/trait/structure.proto +60 -14
  29. package/dist/protobuf/nest/trait/ui.proto +41 -1
  30. package/dist/protobuf/nest/trait/user.proto +6 -1
  31. package/dist/protobuf/nest/trait/voiceassistant.proto +2 -1
  32. package/dist/protobuf/nestlabs/eventingapi/v1.proto +20 -1
  33. package/dist/protobuf/root.proto +1 -0
  34. package/dist/protobuf/wdl.proto +18 -2
  35. package/dist/protobuf/weave/common.proto +2 -1
  36. package/dist/protobuf/weave/trait/heartbeat.proto +41 -1
  37. package/dist/protobuf/weave/trait/power.proto +1 -0
  38. package/dist/protobuf/weave/trait/security.proto +10 -1
  39. package/dist/streamer.js +74 -78
  40. package/dist/system.js +1213 -1264
  41. package/dist/webrtc.js +39 -34
  42. package/package.json +11 -11
  43. package/dist/floodlight.js +0 -97
@@ -1,26 +1,33 @@
1
1
  // Nest Thermostat
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 15/10/2024
5
4
  // Mark Hulskamp
6
5
  'use strict';
7
6
 
8
- // Define our modules
9
- import HomeKitDevice from './HomeKitDevice.js';
10
-
11
7
  // Define nodejs module requirements
12
8
  import path from 'node:path';
9
+ import fs from 'node:fs';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ // Define our modules
13
+ import HomeKitDevice from '../HomeKitDevice.js';
13
14
 
14
- const LOWBATTERYLEVEL = 10; // Low battery level percentage
15
+ // Define constants
16
+ const LOW_BATTERY_LEVEL = 10; // Low battery level percentage
15
17
  const MIN_TEMPERATURE = 9; // Minimum temperature for Nest Thermostat
16
18
  const MAX_TEMPERATURE = 32; // Maximum temperature for Nest Thermostat
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
17
20
 
18
21
  export default class NestThermostat extends HomeKitDevice {
22
+ static TYPE = 'Thermostat';
23
+ static VERSION = '2025.06.15';
24
+
19
25
  batteryService = undefined;
20
26
  occupancyService = undefined;
21
27
  humidityService = undefined;
22
- fanService = undefined;
23
- dehumidifierService = undefined;
28
+ switchService = undefined; // Hotwater heating boost control
29
+ fanService = undefined; // Fan control
30
+ dehumidifierService = undefined; // dehumidifier (only) control
24
31
  externalCool = undefined; // External module function
25
32
  externalHeat = undefined; // External module function
26
33
  externalFan = undefined; // External module function
@@ -31,247 +38,192 @@ export default class NestThermostat extends HomeKitDevice {
31
38
  }
32
39
 
33
40
  // Class functions
34
- async addServices() {
41
+ async setupDevice() {
35
42
  // Setup the thermostat service if not already present on the accessory
36
- this.thermostatService = this.accessory.getService(this.hap.Service.Thermostat);
37
- if (this.thermostatService === undefined) {
38
- this.thermostatService = this.accessory.addService(this.hap.Service.Thermostat, '', 1);
39
- }
43
+ this.thermostatService = this.addHKService(this.hap.Service.Thermostat, '', 1);
40
44
  this.thermostatService.setPrimaryService();
41
45
 
42
- if (this.thermostatService.testCharacteristic(this.hap.Characteristic.StatusActive) === false) {
43
- // Used to indicate active temperature if the thermostat is using its temperature sensor data
44
- // or an external temperature sensor ie: Nest Temperature Sensor
45
- this.thermostatService.addCharacteristic(this.hap.Characteristic.StatusActive);
46
- }
47
- if (this.thermostatService.testCharacteristic(this.hap.Characteristic.StatusFault) === false) {
48
- this.thermostatService.addCharacteristic(this.hap.Characteristic.StatusFault);
49
- }
50
- if (this.thermostatService.testCharacteristic(this.hap.Characteristic.LockPhysicalControls) === false) {
51
- // Setting can only be accessed via Eve App (or other 3rd party).
52
- this.thermostatService.addCharacteristic(this.hap.Characteristic.LockPhysicalControls);
53
- }
54
- if (
55
- this.deviceData?.has_air_filter === true &&
56
- this.thermostatService.testCharacteristic(this.hap.Characteristic.FilterChangeIndication) === false
57
- ) {
58
- // Setup air filter change characteristic
59
- this.thermostatService.addCharacteristic(this.hap.Characteristic.FilterChangeIndication);
60
- }
61
- if (
62
- this.deviceData?.has_air_filter === false &&
63
- this.thermostatService.testCharacteristic(this.hap.Characteristic.FilterChangeIndication) === true
64
- ) {
65
- // No longer configured to have an air filter, so remove characteristic from the accessory
66
- this.thermostatService.removeCharacteristic(this.hap.Characteristic.FilterChangeIndication);
67
- }
46
+ // Setup set characteristics
68
47
 
69
- if (
70
- this.deviceData?.has_humidifier === true &&
71
- this.thermostatService.testCharacteristic(this.hap.Characteristic.TargetRelativeHumidity) === false
72
- ) {
73
- // We have the capability for a humidifier, so setup target humidity characterisitc
74
- this.thermostatService.addCharacteristic(this.hap.Characteristic.TargetRelativeHumidity);
75
- }
48
+ // Used to indicate active temperature if the thermostat is using its temperature sensor data
49
+ // or an external temperature sensor ie: Nest Temperature Sensor
50
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.StatusActive);
76
51
 
77
- if (
78
- this.deviceData?.has_humidifier === false &&
79
- this.thermostatService.testCharacteristic(this.hap.Characteristic.TargetRelativeHumidity) === true
80
- ) {
81
- // No longer configured to use a humdifier, so remove characteristic from the accessory
82
- this.thermostatService.removeCharacteristic(this.hap.Characteristic.TargetRelativeHumidity);
83
- }
84
-
85
- if (this.thermostatService.testCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) === false) {
86
- this.thermostatService.addCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity);
87
- }
52
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.StatusFault);
88
53
 
89
- // Limit prop ranges
90
- if (this.deviceData?.can_cool === false && this.deviceData?.can_heat === true) {
91
- // Can heat only, so set values allowed for mode off/heat
92
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({
93
- validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.HEAT],
94
- });
95
- }
96
- if (this.deviceData?.can_cool === true && this.deviceData?.can_heat === false) {
97
- // Can cool only
98
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({
99
- validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.COOL],
100
- });
101
- }
102
- if (this.deviceData?.can_cool === true && this.deviceData?.can_heat === true) {
103
- // heat and cool
104
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({
105
- validValues: [
106
- this.hap.Characteristic.TargetHeatingCoolingState.OFF,
107
- this.hap.Characteristic.TargetHeatingCoolingState.HEAT,
108
- this.hap.Characteristic.TargetHeatingCoolingState.COOL,
109
- this.hap.Characteristic.TargetHeatingCoolingState.AUTO,
110
- ],
111
- });
112
- }
113
- if (this.deviceData?.can_cool === false && this.deviceData?.can_heat === false) {
114
- // only off mode
115
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({
116
- validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF],
117
- });
118
- }
119
-
120
- // Set default ranges - based on celsuis ranges to which the Nest Thermostat operates
121
- this.thermostatService.getCharacteristic(this.hap.Characteristic.CurrentTemperature).setProps({
122
- minStep: 0.5,
123
- });
124
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetTemperature).setProps({
125
- minStep: 0.5,
126
- minValue: MIN_TEMPERATURE,
127
- maxValue: MAX_TEMPERATURE,
128
- });
129
- this.thermostatService.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature).setProps({
130
- minStep: 0.5,
131
- minValue: MIN_TEMPERATURE,
132
- maxValue: MAX_TEMPERATURE,
133
- });
134
- this.thermostatService.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature).setProps({
135
- minStep: 0.5,
136
- minValue: MIN_TEMPERATURE,
137
- maxValue: MAX_TEMPERATURE,
54
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.LockPhysicalControls, {
55
+ onSet: (value) => {
56
+ this.setChildlock('', value);
57
+ },
58
+ onGet: () => {
59
+ return this.deviceData.temperature_lock === true
60
+ ? this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED
61
+ : this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED;
62
+ },
138
63
  });
139
64
 
140
- // Setup callbacks for characteristics
141
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits).onSet((value) => {
142
- this.setDisplayUnit(value);
143
- });
144
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).onSet((value) => {
145
- this.setMode(value);
146
- });
147
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetTemperature).onSet((value) => {
148
- this.setTemperature(this.hap.Characteristic.TargetTemperature, value);
149
- });
150
- this.thermostatService.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature).onSet((value) => {
151
- this.setTemperature(this.hap.Characteristic.CoolingThresholdTemperature, value);
152
- });
153
- this.thermostatService.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature).onSet((value) => {
154
- this.setTemperature(this.hap.Characteristic.HeatingThresholdTemperature, value);
155
- });
156
- this.thermostatService.getCharacteristic(this.hap.Characteristic.LockPhysicalControls).onSet((value) => {
157
- this.setChildlock('', value);
65
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.CurrentRelativeHumidity, {
66
+ onGet: () => {
67
+ return this.deviceData.current_humidity;
68
+ },
158
69
  });
159
70
 
160
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits).onGet(() => {
161
- return this.deviceData.temperature_scale === 'C'
162
- ? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS
163
- : this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT;
71
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TemperatureDisplayUnits, {
72
+ onSet: (value) => {
73
+ this.setDisplayUnit(value);
74
+ },
75
+ onGet: () => {
76
+ return this.deviceData.temperature_scale === 'C'
77
+ ? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS
78
+ : this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT;
79
+ },
164
80
  });
165
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetTemperature).onGet(() => {
166
- return this.getTemperature(this.hap.Characteristic.TargetTemperature);
81
+
82
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.CurrentTemperature, {
83
+ props: { minStep: 0.5 },
84
+ onGet: () => {
85
+ return this.deviceData.current_temperature;
86
+ },
167
87
  });
168
- this.thermostatService.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature).onGet(() => {
169
- return this.getTemperature(this.hap.Characteristic.CoolingThresholdTemperature);
88
+
89
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TargetTemperature, {
90
+ onSet: (value) => {
91
+ this.setTemperature(this.hap.Characteristic.TargetTemperature, value);
92
+ },
93
+ onGet: () => {
94
+ return this.getTemperature(this.hap.Characteristic.TargetTemperature);
95
+ },
170
96
  });
171
- this.thermostatService.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature).onGet(() => {
172
- return this.getTemperature(this.hap.Characteristic.HeatingThresholdTemperature);
97
+
98
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.CoolingThresholdTemperature, {
99
+ props: {
100
+ minStep: 0.5,
101
+ minValue: MIN_TEMPERATURE,
102
+ maxValue: MAX_TEMPERATURE,
103
+ },
104
+ onSet: (value) => {
105
+ this.setTemperature(this.hap.Characteristic.CoolingThresholdTemperature, value);
106
+ },
107
+ onGet: () => {
108
+ return this.getTemperature(this.hap.Characteristic.CoolingThresholdTemperature);
109
+ },
173
110
  });
174
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).onGet(() => {
175
- return this.getMode();
111
+
112
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.HeatingThresholdTemperature, {
113
+ props: {
114
+ minStep: 0.5,
115
+ minValue: MIN_TEMPERATURE,
116
+ maxValue: MAX_TEMPERATURE,
117
+ },
118
+ onSet: (value) => {
119
+ this.setTemperature(this.hap.Characteristic.HeatingThresholdTemperature, value);
120
+ },
121
+ onGet: () => {
122
+ return this.getTemperature(this.hap.Characteristic.HeatingThresholdTemperature);
123
+ },
176
124
  });
177
- this.thermostatService.getCharacteristic(this.hap.Characteristic.LockPhysicalControls).onGet(() => {
178
- return this.deviceData.temperature_lock === true
179
- ? this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED
180
- : this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED;
125
+
126
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TargetHeatingCoolingState, {
127
+ props: {
128
+ validValues:
129
+ this.deviceData?.can_cool === true && this.deviceData?.can_heat === true
130
+ ? [
131
+ this.hap.Characteristic.TargetHeatingCoolingState.OFF,
132
+ this.hap.Characteristic.TargetHeatingCoolingState.HEAT,
133
+ this.hap.Characteristic.TargetHeatingCoolingState.COOL,
134
+ this.hap.Characteristic.TargetHeatingCoolingState.AUTO,
135
+ ]
136
+ : this.deviceData?.can_heat === true
137
+ ? [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.HEAT]
138
+ : this.deviceData?.can_cool === true
139
+ ? [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.COOL]
140
+ : [this.hap.Characteristic.TargetHeatingCoolingState.OFF],
141
+ },
142
+ onSet: (value) => {
143
+ this.setMode(value);
144
+ },
145
+ onGet: () => {
146
+ return this.getMode();
147
+ },
181
148
  });
182
149
 
183
- // Setup occupancy service if not already present on the accessory
184
- this.occupancyService = this.accessory.getService(this.hap.Service.OccupancySensor);
185
- if (this.occupancyService === undefined) {
186
- this.occupancyService = this.accessory.addService(this.hap.Service.OccupancySensor, '', 1);
150
+ if (this.deviceData?.has_air_filter === true) {
151
+ // We have the capability for an air filter, so setup filter change characterisitc
152
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.FilterChangeIndication);
153
+ }
154
+ if (this.deviceData?.has_air_filter === false) {
155
+ // No longer configured to have an air filter, so remove characteristic from the accessory
156
+ this.thermostatService.removeCharacteristic(this.hap.Characteristic.FilterChangeIndication);
187
157
  }
158
+
159
+ // Setup occupancy service if not already present on the accessory
160
+ this.occupancyService = this.addHKService(this.hap.Service.OccupancySensor, '', 1);
188
161
  this.thermostatService.addLinkedService(this.occupancyService);
189
162
 
190
163
  // Setup battery service if not already present on the accessory
191
- this.batteryService = this.accessory.getService(this.hap.Service.Battery);
192
- if (this.batteryService === undefined) {
193
- this.batteryService = this.accessory.addService(this.hap.Service.Battery, '', 1);
194
- }
164
+ this.batteryService = this.addHKService(this.hap.Service.Battery, '', 1);
195
165
  this.batteryService.setHiddenService(true);
196
166
  this.thermostatService.addLinkedService(this.batteryService);
197
167
 
198
168
  // Setup fan service if supported by the thermostat and not already present on the accessory
199
- this.fanService = this.accessory.getService(this.hap.Service.Fanv2);
200
169
  if (this.deviceData?.has_fan === true) {
201
- if (this.fanService === undefined) {
202
- this.fanService = this.accessory.addService(this.hap.Service.Fanv2, '', 1);
203
- }
204
- if (this.fanService.testCharacteristic(this.hap.Characteristic.RotationSpeed) === false) {
205
- this.fanService.addCharacteristic(this.hap.Characteristic.RotationSpeed);
206
- }
207
- this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).setProps({
208
- minStep: 100 / this.deviceData.fan_max_speed,
209
- });
210
-
211
- this.thermostatService.addLinkedService(this.fanService);
212
-
213
- this.fanService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => {
214
- this.setFan(
215
- value,
216
- value === this.hap.Characteristic.Active.ACTIVE ? (this.deviceData.fan_timer_speed / this.deviceData.fan_max_speed) * 100 : 0,
217
- );
218
- });
219
- this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).onSet((value) => {
220
- this.setFan(value !== 0 ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE, value);
221
- });
222
- this.fanService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => {
223
- return this.deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE;
224
- });
225
- this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).onGet(() => {
226
- return (this.deviceData.fan_timer_speed / this.deviceData.fan_max_speed) * 100;
227
- });
170
+ this.#setupFan();
228
171
  }
229
- if (this.deviceData?.has_fan === false && this.fanService !== undefined) {
172
+ if (this.deviceData?.has_fan === false) {
230
173
  // No longer have a Fan configured and service present, so removed it
231
- this.accessory.removeService(this.fanService);
174
+ this.fanService = this.accessory.getService(this.hap.Service.Fanv2);
175
+ if (this.fanService !== undefined) {
176
+ this.accessory.removeService(this.fanService);
177
+ }
232
178
  this.fanService = undefined;
233
179
  }
234
180
 
235
181
  // Setup dehumifider service if supported by the thermostat and not already present on the accessory
236
- this.dehumidifierService = this.accessory.getService(this.hap.Service.HumidifierDehumidifier);
237
182
  if (this.deviceData?.has_dehumidifier === true) {
238
- if (this.dehumidifierService === undefined) {
239
- this.dehumidifierService = this.accessory.addService(this.hap.Service.HumidifierDehumidifier, '', 1);
240
- }
241
- this.thermostatService.addLinkedService(this.dehumidifierService);
242
-
243
- this.dehumidifierService.getCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState).setProps({
244
- validValues: [this.hap.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER],
245
- });
246
- this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => {
247
- this.setDehumidifier(value);
248
- });
249
- this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => {
250
- return this.deviceData.dehumidifier_state === true
251
- ? this.hap.Characteristic.Active.ACTIVE
252
- : this.hap.Characteristic.Active.INACTIVE;
253
- });
183
+ this.#setupDehumidifier();
254
184
  }
255
- if (this.deviceData?.has_dehumidifier === false && this.dehumidifierService !== undefined) {
185
+ if (this.deviceData?.has_dehumidifier === false) {
256
186
  // No longer have a dehumidifier configured and service present, so removed it
257
- this.accessory.removeService(this.dehumidifierService);
187
+ this.dehumidifierService = this.accessory.getService(this.hap.Service.HumidifierDehumidifier);
188
+ if (this.dehumidifierService !== undefined) {
189
+ this.accessory.removeService(this.dehumidifierService);
190
+ }
258
191
  this.dehumidifierService = undefined;
259
192
  }
260
193
 
261
194
  // Setup humdity service if configured to be seperate and not already present on the accessory
262
- this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor);
263
195
  if (this.deviceData?.humiditySensor === true) {
264
- if (this.humidityService === undefined) {
265
- this.humidityService = this.accessory.addService(this.hap.Service.HumiditySensor, '', 1);
266
- }
196
+ this.humidityService = this.addHKService(this.hap.Service.HumiditySensor, '', 1);
267
197
  this.thermostatService.addLinkedService(this.humidityService);
198
+
199
+ this.addHKCharacteristic(this.humidityService, this.hap.Characteristic.CurrentRelativeHumidity, {
200
+ onGet: () => {
201
+ return this.deviceData.current_humidity;
202
+ },
203
+ });
268
204
  }
269
- if (this.deviceData?.humiditySensor === false && this.humidityService !== undefined) {
205
+ if (this.deviceData?.humiditySensor === false) {
270
206
  // No longer have a seperate humidity sensor configure and service present, so removed it
271
- this.accessory.removeService(this.humidityService);
207
+ this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor);
208
+ if (this.humidityService !== undefined) {
209
+ this.accessory.removeService(this.humidityService);
210
+ }
272
211
  this.humidityService = undefined;
273
212
  }
274
213
 
214
+ // Setup hotwater heating boost service if supported by the thermostat and not already present on the accessory
215
+ if (this.deviceData?.has_hot_water_control === true) {
216
+ this.#setupHotwaterBoost();
217
+ }
218
+ if (this.deviceData?.has_hot_water_control === false) {
219
+ // No longer have hotwater heating boost configured and service present, so removed it
220
+ this.switchService = this.accessory.getService(this.hap.Service.Switch);
221
+ if (this.switchService !== undefined) {
222
+ this.accessory.removeService(this.switchService);
223
+ }
224
+ this.switchService = undefined;
225
+ }
226
+
275
227
  // Setup linkage to EveHome app if configured todo so
276
228
  if (
277
229
  this.deviceData?.eveHistory === true &&
@@ -288,42 +240,17 @@ export default class NestThermostat extends HomeKitDevice {
288
240
  // Attempt to load any external modules for this thermostat
289
241
  // We support external cool/heat/fan/dehumidifier module functions
290
242
  // This is all undocumented on how to use, as its for my specific use case :-)
291
- const loadExternalModule = async (module) => {
292
- if (typeof module !== 'string' || module === '') {
293
- return;
294
- }
295
-
296
- let loadedModule = undefined;
297
- try {
298
- let values = module.match(/('.*?'|[^' ]+)(?=\s* |\s*$)/g);
299
- let script = path.resolve(values[0]); // external library name
300
- let options = values.slice(1); // options to be passed into the external library
301
- let externalModule = await import(script);
302
- if (typeof externalModule?.default === 'function') {
303
- loadedModule = externalModule.default(this.log, options);
304
- }
305
- // eslint-disable-next-line no-unused-vars
306
- } catch (error) {
307
- this?.log?.warn && this.log.warn('Failed to load specified external module for thermostat "%s"', module);
308
- }
309
-
310
- return loadedModule;
311
- };
312
-
313
- this.externalCool = await loadExternalModule(this.deviceData?.externalCool);
314
- this.externalHeat = await loadExternalModule(this.deviceData?.externalHeat);
315
- this.externalFan = await loadExternalModule(this.deviceData?.externalFan);
316
- this.externalDehumidifier = await loadExternalModule(this.deviceData?.externalDehumidifier);
317
-
318
- // Create extra details for output
319
- let postSetupDetails = [];
320
- this.humidityService !== undefined && postSetupDetails.push('Seperate humidity sensor');
321
- this.externalCool !== undefined && postSetupDetails.push('Using external cooling module');
322
- this.externalHeat !== undefined && postSetupDetails.push('Using external heating module');
323
- this.externalFan !== undefined && postSetupDetails.push('Using external fan module');
324
- this.externalDehumidifier !== undefined && postSetupDetails.push('Using external dehumidification module');
325
-
326
- return postSetupDetails;
243
+ this.externalCool = await this.#loadExternalModule(this.deviceData?.externalCool, ['cool', 'off']);
244
+ this.externalHeat = await this.#loadExternalModule(this.deviceData?.externalHeat, ['heat', 'off']);
245
+ this.externalFan = await this.#loadExternalModule(this.deviceData?.externalFan, ['fan', 'off']);
246
+ this.externalDehumidifier = await this.#loadExternalModule(this.deviceData?.externalDehumidifier, ['dehumidifier', 'off']);
247
+
248
+ // Extra setup details for output
249
+ this.humidityService !== undefined && this.postSetupDetail('Seperate humidity sensor');
250
+ this.externalCool !== undefined && this.postSetupDetail('Using external cooling module');
251
+ this.externalHeat !== undefined && this.postSetupDetail('Using external heating module');
252
+ this.externalFan !== undefined && this.postSetupDetail('Using external fan module');
253
+ this.externalDehumidifier !== undefined && this.postSetupDetail('Using external dehumidification module');
327
254
  }
328
255
 
329
256
  setFan(fanState, speed) {
@@ -339,30 +266,42 @@ export default class NestThermostat extends HomeKitDevice {
339
266
  this.fanService.updateCharacteristic(this.hap.Characteristic.Active, fanState);
340
267
  this.fanService.updateCharacteristic(this.hap.Characteristic.RotationSpeed, speed);
341
268
 
342
- this?.log?.info &&
343
- this.log.info(
344
- 'Set fan on thermostat "%s" to "%s"',
345
- this.deviceData.description,
346
- fanState === this.hap.Characteristic.Active.ACTIVE ? 'On with fan speed of ' + speed + '%' : 'Off',
347
- );
269
+ this?.log?.info?.(
270
+ 'Set fan on thermostat "%s" to "%s"',
271
+ this.deviceData.description,
272
+ fanState === this.hap.Characteristic.Active.ACTIVE ? 'On with fan speed of ' + speed + '%' : 'Off',
273
+ );
348
274
  }
349
275
  }
350
276
 
351
277
  setDehumidifier(dehumidiferState) {
352
278
  this.set({
353
279
  uuid: this.deviceData.nest_google_uuid,
354
- dehumidifer_State: dehumidiferState === this.hap.Characteristic.Active.ACTIVE ? true : false,
280
+ dehumidifier_state: dehumidiferState === this.hap.Characteristic.Active.ACTIVE ? true : false,
355
281
  });
356
282
  this.dehumidifierService.updateCharacteristic(this.hap.Characteristic.Active, dehumidiferState);
357
283
 
358
- this?.log?.info &&
359
- this.log.info(
360
- 'Set dehumidifer on thermostat "%s" to "%s"',
361
- this.deviceData.description,
362
- dehumidiferState === this.hap.Characteristic.Active.ACTIVE
363
- ? 'On with target humidity level of ' + this.deviceData.target_humidity + '%'
364
- : 'Off',
365
- );
284
+ this?.log?.info?.(
285
+ 'Set dehumidifer on thermostat "%s" to "%s"',
286
+ this.deviceData.description,
287
+ dehumidiferState === this.hap.Characteristic.Active.ACTIVE
288
+ ? 'On with target humidity level of ' + this.deviceData.target_humidity + '%'
289
+ : 'Off',
290
+ );
291
+ }
292
+
293
+ setHotwaterBoost(hotwaterState) {
294
+ this.set({
295
+ uuid: this.deviceData.nest_google_uuid,
296
+ hot_water_boost_active: { state: hotwaterState === true, time: this.deviceData.hotWaterBoostTime },
297
+ });
298
+ this.switchService.updateCharacteristic(this.hap.Characteristic.On, hotwaterState);
299
+
300
+ this?.log?.info?.(
301
+ 'Set hotwater boost heating on thermostat "%s" to "%s"',
302
+ this.deviceData.description,
303
+ hotwaterState === true ? 'On for ' + formatDuration(this.deviceData.hotWaterBoostTime) : 'Off',
304
+ );
366
305
  }
367
306
 
368
307
  setDisplayUnit(temperatureUnit) {
@@ -372,12 +311,11 @@ export default class NestThermostat extends HomeKitDevice {
372
311
  });
373
312
  this.thermostatService.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, temperatureUnit);
374
313
 
375
- this?.log?.info &&
376
- this.log.info(
377
- 'Set temperature units on thermostat "%s" to "%s"',
378
- this.deviceData.description,
379
- temperatureUnit === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? '°C' : '°F',
380
- );
314
+ this?.log?.info?.(
315
+ 'Set temperature units on thermostat "%s" to "%s"',
316
+ this.deviceData.description,
317
+ temperatureUnit === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? '°C' : '°F',
318
+ );
381
319
  }
382
320
 
383
321
  setMode(thermostatMode) {
@@ -443,7 +381,7 @@ export default class NestThermostat extends HomeKitDevice {
443
381
 
444
382
  this.set({ uuid: this.deviceData.nest_google_uuid, hvac_mode: mode });
445
383
 
446
- this?.log?.info && this.log.info('Set mode on "%s" to "%s"', this.deviceData.description, mode);
384
+ this?.log?.info?.('Set mode on "%s" to "%s"', this.deviceData.description, mode);
447
385
  }
448
386
  }
449
387
 
@@ -479,17 +417,16 @@ export default class NestThermostat extends HomeKitDevice {
479
417
  ) {
480
418
  this.set({ uuid: this.deviceData.nest_google_uuid, target_temperature: temperature });
481
419
 
482
- this?.log?.info &&
483
- this.log.info(
484
- 'Set %s%s temperature on "%s" to "%s °C"',
485
- this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '',
486
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value ===
487
- this.hap.Characteristic.TargetHeatingCoolingState.HEAT
488
- ? 'heating'
489
- : 'cooling',
490
- this.deviceData.description,
491
- temperature,
492
- );
420
+ this?.log?.info?.(
421
+ 'Set %s%s temperature on "%s" to "%s °C"',
422
+ this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '',
423
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value ===
424
+ this.hap.Characteristic.TargetHeatingCoolingState.HEAT
425
+ ? 'heating'
426
+ : 'cooling',
427
+ this.deviceData.description,
428
+ temperature,
429
+ );
493
430
  }
494
431
  if (
495
432
  characteristic.UUID === this.hap.Characteristic.HeatingThresholdTemperature.UUID &&
@@ -498,13 +435,12 @@ export default class NestThermostat extends HomeKitDevice {
498
435
  ) {
499
436
  this.set({ uuid: this.deviceData.nest_google_uuid, target_temperature_low: temperature });
500
437
 
501
- this?.log?.info &&
502
- this.log.info(
503
- 'Set %sheating temperature on "%s" to "%s °C"',
504
- this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '',
505
- this.deviceData.description,
506
- temperature,
507
- );
438
+ this?.log?.info?.(
439
+ 'Set %sheating temperature on "%s" to "%s °C"',
440
+ this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '',
441
+ this.deviceData.description,
442
+ temperature,
443
+ );
508
444
  }
509
445
  if (
510
446
  characteristic.UUID === this.hap.Characteristic.CoolingThresholdTemperature.UUID &&
@@ -513,13 +449,12 @@ export default class NestThermostat extends HomeKitDevice {
513
449
  ) {
514
450
  this.set({ uuid: this.deviceData.nest_google_uuid, target_temperature_high: temperature });
515
451
 
516
- this?.log?.info &&
517
- this.log.info(
518
- 'Set %scooling temperature on "%s" to "%s °C"',
519
- this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '',
520
- this.deviceData.description,
521
- temperature,
522
- );
452
+ this?.log?.info?.(
453
+ 'Set %scooling temperature on "%s" to "%s °C"',
454
+ this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '',
455
+ this.deviceData.description,
456
+ temperature,
457
+ );
523
458
  }
524
459
 
525
460
  this.thermostatService.updateCharacteristic(characteristic, temperature); // Update HomeKit with value
@@ -567,15 +502,14 @@ export default class NestThermostat extends HomeKitDevice {
567
502
  temperature_lock: value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? true : false,
568
503
  });
569
504
 
570
- this?.log?.info &&
571
- this.log.info(
572
- 'Setting Childlock on "%s" to "%s"',
573
- this.deviceData.description,
574
- value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? 'Enabled' : 'Disabled',
575
- );
505
+ this?.log?.info?.(
506
+ 'Setting Childlock on "%s" to "%s"',
507
+ this.deviceData.description,
508
+ value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? 'Enabled' : 'Disabled',
509
+ );
576
510
  }
577
511
 
578
- updateServices(deviceData) {
512
+ updateDevice(deviceData) {
579
513
  if (
580
514
  typeof deviceData !== 'object' ||
581
515
  this.thermostatService === undefined ||
@@ -630,7 +564,7 @@ export default class NestThermostat extends HomeKitDevice {
630
564
  this.batteryService.updateCharacteristic(this.hap.Characteristic.BatteryLevel, deviceData.battery_level);
631
565
  this.batteryService.updateCharacteristic(
632
566
  this.hap.Characteristic.StatusLowBattery,
633
- deviceData.battery_level > LOWBATTERYLEVEL
567
+ deviceData.battery_level > LOW_BATTERY_LEVEL
634
568
  ? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
635
569
  : this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW,
636
570
  );
@@ -661,23 +595,7 @@ export default class NestThermostat extends HomeKitDevice {
661
595
  if (deviceData.has_fan !== this.deviceData.has_fan) {
662
596
  if (deviceData.has_fan === true && this.deviceData.has_fan === false && this.fanService === undefined) {
663
597
  // Fan has been added
664
- this.fanService = this.accessory.addService(this.hap.Service.Fanv2, '', 1);
665
-
666
- if (this.fanService.testCharacteristic(this.hap.Characteristic.RotationSpeed) === false) {
667
- this.fanService.addCharacteristic(this.hap.Characteristic.RotationSpeed);
668
- }
669
- this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).setProps({
670
- minStep: 100 / this.deviceData.fan_max_speed,
671
- });
672
- this.thermostatService.addLinkedService(this.fanService);
673
-
674
- this.fanService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => {
675
- this.setFan(value);
676
- });
677
-
678
- this.fanService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => {
679
- return this.deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE;
680
- });
598
+ this.#setupFan();
681
599
  }
682
600
  if (deviceData.has_fan === false && this.deviceData.has_fan === true && this.fanService !== undefined) {
683
601
  // Fan has been removed
@@ -685,34 +603,18 @@ export default class NestThermostat extends HomeKitDevice {
685
603
  this.fanService = undefined;
686
604
  }
687
605
 
688
- this?.log?.info &&
689
- this.log.info(
690
- 'Fan setup on thermostat "%s" has changed. Fan was',
691
- deviceData.description,
692
- this.fanService === undefined ? 'removed' : 'added',
693
- );
606
+ this?.log?.info?.(
607
+ 'Fan setup on thermostat "%s" has changed. Fan was',
608
+ deviceData.description,
609
+ this.fanService === undefined ? 'removed' : 'added',
610
+ );
694
611
  }
695
612
 
696
613
  // Check for dehumidifer setup change on thermostat
697
614
  if (deviceData.has_dehumidifier !== this.deviceData.has_dehumidifier) {
698
615
  if (deviceData.has_dehumidifier === true && this.deviceData.has_dehumidifier === false && this.dehumidifierService === undefined) {
699
616
  // Dehumidifier has been added
700
- this.dehumidifierService = this.accessory.addService(this.hap.Service.HumidifierDehumidifier, '', 1);
701
- this.thermostatService.addLinkedService(this.dehumidifierService);
702
-
703
- this.dehumidifierService.getCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState).setProps({
704
- validValues: [this.hap.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER],
705
- });
706
-
707
- this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => {
708
- this.setDehumidifier(value);
709
- });
710
-
711
- this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => {
712
- return this.deviceData.dehumidifier_state === true
713
- ? this.hap.Characteristic.Active.ACTIVE
714
- : this.hap.Characteristic.Active.INACTIVE;
715
- });
617
+ this.#setupDehumidifier();
716
618
  }
717
619
  if (deviceData.has_dehumidifier === false && this.deviceData.has_dehumidifier === true && this.dehumidifierService !== undefined) {
718
620
  // Dehumidifer has been removed
@@ -720,12 +622,41 @@ export default class NestThermostat extends HomeKitDevice {
720
622
  this.dehumidifierService = undefined;
721
623
  }
722
624
 
723
- this?.log?.info &&
724
- this.log.info(
725
- 'Dehumidifier setup on thermostat "%s" has changed. Dehumidifier was',
726
- deviceData.description,
727
- this.dehumidifierService === undefined ? 'removed' : 'added',
728
- );
625
+ this?.log?.info?.(
626
+ 'Dehumidifier setup on thermostat "%s" has changed. Dehumidifier was',
627
+ deviceData.description,
628
+ this.dehumidifierService === undefined ? 'removed' : 'added',
629
+ );
630
+ }
631
+
632
+ // Check for hotwater heating boost setup change on thermostat
633
+ if (deviceData.has_hot_water_control !== this.deviceData.has_hot_water_control) {
634
+ if (
635
+ deviceData.has_hot_water_control === true &&
636
+ this.deviceData.has_hot_water_control === false &&
637
+ this.switchService === undefined
638
+ ) {
639
+ // hotwater heating boost has been added
640
+ this.switchService = this.accessory.getService(this.hap.Service.Switch);
641
+ if (this.deviceData.has_hot_water_control === true) {
642
+ this.#setupHotwaterBoost();
643
+ }
644
+ if (
645
+ deviceData.has_hot_water_control === false &&
646
+ this.deviceData.has_hot_water_control === true &&
647
+ this.switchService !== undefined
648
+ ) {
649
+ // hotwater heating boost has been removed
650
+ this.accessory.removeService(this.switchService);
651
+ this.switchService = undefined;
652
+ }
653
+ }
654
+
655
+ this?.log?.info?.(
656
+ 'hotwater heating boost setup on thermostat "%s" has changed. Hotwater heating boost was',
657
+ deviceData.description,
658
+ this.switchService === undefined ? 'removed' : 'added',
659
+ );
729
660
  }
730
661
 
731
662
  if (deviceData.can_cool !== this.deviceData.can_cool || deviceData.can_heat !== this.deviceData.can_heat) {
@@ -762,7 +693,7 @@ export default class NestThermostat extends HomeKitDevice {
762
693
  });
763
694
  }
764
695
 
765
- this?.log?.info && this.log.info('Heating/cooling setup on thermostat on "%s" has changed', deviceData.description);
696
+ this?.log?.info?.('Heating/cooling setup on thermostat on "%s" has changed', deviceData.description);
766
697
  }
767
698
 
768
699
  // Update current mode temperatures
@@ -808,7 +739,7 @@ export default class NestThermostat extends HomeKitDevice {
808
739
  historyEntry.target = { low: deviceData.target_temperature_low, high: deviceData.target_temperature_high };
809
740
  }
810
741
  if (deviceData.can_cool === false && deviceData.can_heat === false && deviceData.hvac_mode.toUpperCase() === 'OFF') {
811
- // off mode.
742
+ // off mode
812
743
  this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.target_temperature);
813
744
  this.thermostatService.updateCharacteristic(
814
745
  this.hap.Characteristic.TargetHeatingCoolingState,
@@ -819,22 +750,18 @@ export default class NestThermostat extends HomeKitDevice {
819
750
 
820
751
  // Update current state
821
752
  if (deviceData.hvac_state.toUpperCase() === 'HEATING') {
822
- if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && this.externalCool !== undefined) {
753
+ if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && typeof this.externalCool?.off === 'function') {
823
754
  // Switched to heating mode and external cooling external code was being used, so stop cooling via cooling external code
824
- if (typeof this.externalCool.off === 'function') {
825
- this.externalCool.off();
826
- }
755
+ this.externalCool.off();
827
756
  }
828
757
  if (
829
758
  (this.deviceData.hvac_state.toUpperCase() !== 'HEATING' ||
830
759
  deviceData.target_temperature_low !== this.deviceData.target_temperature_low) &&
831
- this.externalHeat !== undefined
760
+ typeof this.externalHeat?.heat === 'function'
832
761
  ) {
833
762
  // Switched to heating mode and external heating external code is being used
834
763
  // Start heating via heating external code OR adjust heating target temperature due to change
835
- if (typeof this.externalHeat.heat === 'function') {
836
- this.externalHeat.heat(deviceData.deviceData.target_temperature_low);
837
- }
764
+ this.externalHeat.heat(deviceData.target_temperature_low);
838
765
  }
839
766
  this.thermostatService.updateCharacteristic(
840
767
  this.hap.Characteristic.CurrentHeatingCoolingState,
@@ -843,22 +770,18 @@ export default class NestThermostat extends HomeKitDevice {
843
770
  historyEntry.status = 2; // heating
844
771
  }
845
772
  if (deviceData.hvac_state.toUpperCase() === 'COOLING') {
846
- if (this.deviceData.hvac_state.toUpperCase() === 'HEATING' && this.externalHeat !== undefined) {
773
+ if (this.deviceData.hvac_state.toUpperCase() === 'HEATING' && typeof this.externalHeat?.off === 'function') {
847
774
  // Switched to cooling mode and external heating external code was being used, so stop heating via heating external code
848
- if (typeof this.externalHeat.off === 'function') {
849
- this.externalHeat.off();
850
- }
775
+ this.externalHeat.off();
851
776
  }
852
777
  if (
853
778
  (this.deviceData.hvac_state.toUpperCase() !== 'COOLING' ||
854
779
  deviceData.target_temperature_high !== this.deviceData.target_temperature_high) &&
855
- this.externalCool !== undefined
780
+ typeof this.externalCool?.cool === 'function'
856
781
  ) {
857
782
  // Switched to cooling mode and external cooling external code is being used
858
783
  // Start cooling via cooling external code OR adjust cooling target temperature due to change
859
- if (typeof this.externalCool.cool === 'function') {
860
- this.externalCool.cool(deviceData.target_temperature_high);
861
- }
784
+ this.externalCool.cool(deviceData.target_temperature_high);
862
785
  }
863
786
  this.thermostatService.updateCharacteristic(
864
787
  this.hap.Characteristic.CurrentHeatingCoolingState,
@@ -867,17 +790,13 @@ export default class NestThermostat extends HomeKitDevice {
867
790
  historyEntry.status = 3; // cooling
868
791
  }
869
792
  if (deviceData.hvac_state.toUpperCase() === 'OFF') {
870
- if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && this.externalCool !== undefined) {
871
- // Switched to off mode and external cooling external code was being used, so stop cooling via cooling external code
872
- if (typeof this.externalCool.off === 'function') {
873
- this.externalCool.off();
874
- }
793
+ if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && typeof this.externalCool?.off === 'function') {
794
+ // Switched to off mode and external cooling external code was being used, so stop cooling via cooling external code{
795
+ this.externalCool.off();
875
796
  }
876
- if (this.deviceData.hvac_state.toUpperCase() === 'HEATING' && this.externalHeat !== undefined) {
797
+ if (this.deviceData.hvac_state.toUpperCase() === 'HEATING' && typeof this.externalHeat?.off === 'function') {
877
798
  // Switched to off mode and external heating external code was being used, so stop heating via heating external code
878
- if (typeof this.externalHeat.heat === 'function') {
879
- this.externalHeat.off();
880
- }
799
+ this.externalHeat.off();
881
800
  }
882
801
  this.thermostatService.updateCharacteristic(
883
802
  this.hap.Characteristic.CurrentHeatingCoolingState,
@@ -885,18 +804,16 @@ export default class NestThermostat extends HomeKitDevice {
885
804
  );
886
805
  historyEntry.status = 0; // off
887
806
  }
807
+
888
808
  if (this.fanService !== undefined) {
889
- if (this.deviceData.fan_state === false && deviceData.fan_state === true && this.externalFan !== undefined) {
809
+ // fan status on or off
810
+ if (this.deviceData.fan_state === false && deviceData.fan_state === true && typeof this.externalFan?.fan === 'function') {
890
811
  // Fan mode was switched on and external fan external code is being used, so start fan via fan external code
891
- if (typeof this.externalFan.fan === 'function') {
892
- this.externalFan.fan(0); // Fan speed will be auto
893
- }
812
+ this.externalFan.fan(0); // Fan speed will be auto
894
813
  }
895
- if (this.deviceData.fan_state === true && deviceData.fan_state === false && this.externalFan !== undefined) {
814
+ if (this.deviceData.fan_state === true && deviceData.fan_state === false && typeof this.externalFan?.off === 'function') {
896
815
  // Fan mode was switched off and external fan external code was being used, so stop fan via fan external code
897
- if (typeof this.externalFan.off === 'function') {
898
- this.externalFan.off();
899
- }
816
+ this.externalFan.off();
900
817
  }
901
818
 
902
819
  this.fanService.updateCharacteristic(
@@ -907,40 +824,43 @@ export default class NestThermostat extends HomeKitDevice {
907
824
  this.fanService.updateCharacteristic(
908
825
  this.hap.Characteristic.Active,
909
826
  deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
910
- ); // fan status on or off
827
+ );
911
828
  historyEntry.status = 1; // fan
912
829
  }
830
+
913
831
  if (this.dehumidifierService !== undefined) {
832
+ // dehumidifier status on or off
914
833
  if (
915
834
  this.deviceData.dehumidifier_state === false &&
916
835
  deviceData.dehumidifier_state === true &&
917
- this.externalDehumidifier !== undefined
836
+ typeof this.externalDehumidifier?.dehumidifier === 'function'
918
837
  ) {
919
838
  // Dehumidifier mode was switched on and external dehumidifier external code is being used
920
839
  // Start dehumidifier via dehumidifier external code
921
- if (typeof this.externalDehumidifier.dehumififier === 'function') {
922
- this.externalDehumidifier.dehumififier(0);
923
- }
840
+ this.externalDehumidifier.dehumidifier(0);
924
841
  }
925
842
  if (
926
843
  this.deviceData.dehumidifier_state === true &&
927
844
  deviceData.dehumidifier_state === false &&
928
- this.externalDehumidifier !== undefined
845
+ typeof this.externalDehumidifier?.off === 'function'
929
846
  ) {
930
847
  // Dehumidifier mode was switched off and external dehumidifier external code was being used
931
848
  // Stop dehumidifier via dehumidifier external code
932
- if (typeof this.externalDehumidifier.off === 'function') {
933
- this.externalDehumidifier.off();
934
- }
849
+ this.externalDehumidifier.off();
935
850
  }
936
851
 
937
852
  this.dehumidifierService.updateCharacteristic(
938
853
  this.hap.Characteristic.Active,
939
854
  deviceData.dehumidifier_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
940
- ); // dehumidifier status on or off
855
+ );
941
856
  historyEntry.status = 4; // dehumidifier
942
857
  }
943
858
 
859
+ if (this.switchService !== undefined) {
860
+ // Hotwater boost status on or off
861
+ this.switchService.updateCharacteristic(this.hap.Characteristic.On, deviceData.hot_water_boost_active === true);
862
+ }
863
+
944
864
  // Log thermostat metrics to history only if changed to previous recording
945
865
  if (this.thermostatService !== undefined && typeof this.historyService?.addHistory === 'function') {
946
866
  let tempEntry = this.historyService.lastHistory(this.thermostatService);
@@ -1056,4 +976,127 @@ export default class NestThermostat extends HomeKitDevice {
1056
976
  //});
1057
977
  }
1058
978
  }
979
+
980
+ #setupFan() {
981
+ this.fanService = this.addHKService(this.hap.Service.Fanv2, '', 1);
982
+ this.addHKCharacteristic(this.hap.Service.Fanv2, this.hap.Characteristic.RotationSpeed);
983
+ this.thermostatService.addLinkedService(this.fanService);
984
+
985
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.Active, {
986
+ onSet: (value) =>
987
+ this.setFan(
988
+ value,
989
+ value === this.hap.Characteristic.Active.ACTIVE ? (this.deviceData.fan_timer_speed / this.deviceData.fan_max_speed) * 100 : 0,
990
+ ),
991
+ onGet: () => {
992
+ return this.deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE;
993
+ },
994
+ });
995
+
996
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.RotationSpeed, {
997
+ props: { minStep: 100 / this.deviceData.fan_max_speed },
998
+ onSet: (value) => this.setFan(value !== 0 ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE, value),
999
+ onGet: () => {
1000
+ return (this.deviceData.fan_timer_speed / this.deviceData.fan_max_speed) * 100;
1001
+ },
1002
+ });
1003
+ }
1004
+
1005
+ #setupDehumidifier() {
1006
+ this.dehumidifierService = this.addHKService(this.hap.Service.HumidifierDehumidifier, '', 1);
1007
+ this.thermostatService.addLinkedService(this.dehumidifierService);
1008
+
1009
+ this.addHKCharacteristic(this.dehumidifierService, this.hap.Characteristic.TargetHumidifierDehumidifierState, {
1010
+ props: { validValues: [this.hap.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER] },
1011
+ });
1012
+
1013
+ this.addHKCharacteristic(this.dehumidifierService, this.hap.Characteristic.Active, {
1014
+ onSet: (value) => this.setDehumidifier(value),
1015
+ onGet: () => {
1016
+ return this.deviceData.dehumidifier_state === true
1017
+ ? this.hap.Characteristic.Active.ACTIVE
1018
+ : this.hap.Characteristic.Active.INACTIVE;
1019
+ },
1020
+ });
1021
+ }
1022
+
1023
+ #setupHotwaterBoost() {
1024
+ this.switchService = this.addHKService(this.hap.Service.Switch, '', 1);
1025
+ this.thermostatService.addLinkedService(this.switchService);
1026
+
1027
+ this.addHKCharacteristic(this.switchService, this.hap.Characteristic.On, {
1028
+ onSet: (value) => this.setHotwaterBoost(value),
1029
+ onGet: () => {
1030
+ return this.deviceData?.hot_water_boost_active === true;
1031
+ },
1032
+ });
1033
+ }
1034
+
1035
+ async #loadExternalModule(module, expectedFunctions = []) {
1036
+ if (typeof module !== 'string' || module === '' || Array.isArray(expectedFunctions) === false) {
1037
+ return undefined;
1038
+ }
1039
+
1040
+ // Helper to resolve a module path, defaulting to plugin dir and falling back from .mjs to .js
1041
+ const resolveModulePath = async (basePath) => {
1042
+ let hasExtension = path.extname(basePath) !== '';
1043
+ let isRelative = basePath.startsWith('./') || basePath.startsWith('../');
1044
+ let isAbsolute = path.isAbsolute(basePath);
1045
+ let resolvedBase = isAbsolute ? basePath : isRelative ? path.resolve(basePath) : path.resolve(__dirname, basePath);
1046
+ let finalPath = resolvedBase;
1047
+
1048
+ if (hasExtension === false) {
1049
+ let mjsPath = `${resolvedBase}.mjs`;
1050
+ let jsPath = `${resolvedBase}.js`;
1051
+
1052
+ try {
1053
+ await fs.access(mjsPath);
1054
+ finalPath = mjsPath;
1055
+ } catch {
1056
+ try {
1057
+ await fs.access(jsPath);
1058
+ finalPath = jsPath;
1059
+ } catch {
1060
+ finalPath = mjsPath; // fallback to mjs even if not found
1061
+ }
1062
+ }
1063
+ }
1064
+ return finalPath;
1065
+ };
1066
+
1067
+ let loadedModule = undefined;
1068
+
1069
+ try {
1070
+ let values = module.match(/'[^']*'|[^\s]+/g)?.map((v) => v.replace(/^'(.*)'$/, '$1')) || [];
1071
+ let script = await resolveModulePath(values[0]);
1072
+ let options = values.slice(1);
1073
+ let externalModule = await import(script);
1074
+
1075
+ if (typeof externalModule?.default === 'function') {
1076
+ let moduleExports = externalModule.default(this.log, options);
1077
+ let valid = Object.fromEntries(
1078
+ expectedFunctions.filter((fn) => typeof moduleExports[fn] === 'function').map((fn) => [fn, moduleExports[fn]]),
1079
+ );
1080
+ loadedModule = Object.keys(valid).length > 0 ? valid : undefined;
1081
+ }
1082
+ // eslint-disable-next-line no-unused-vars
1083
+ } catch (error) {
1084
+ let shortName =
1085
+ typeof module === 'string'
1086
+ ? module
1087
+ .trim()
1088
+ .match(/'[^']*'|[^\s]+/)?.[0]
1089
+ ?.replace(/^'(.*)'$/, '$1')
1090
+ : '';
1091
+ this?.log?.warn?.('Failed to load external module "%s" for thermostat "%s"', shortName, this.deviceData.description);
1092
+ }
1093
+
1094
+ return loadedModule;
1095
+ }
1096
+ }
1097
+
1098
+ function formatDuration(seconds) {
1099
+ return `${
1100
+ seconds >= 3600 ? `${Math.floor(seconds / 3600)} hr${Math.floor(seconds / 3600) > 1 ? 's' : ''} ` : ''
1101
+ }${Math.floor((seconds % 3600) / 60)} min${Math.floor((seconds % 3600) / 60) !== 1 ? 's' : ''}`;
1059
1102
  }