homebridge-nest-accfactory 0.0.4-a

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 (88) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +176 -0
  3. package/README.md +121 -0
  4. package/config.schema.json +107 -0
  5. package/dist/HomeKitDevice.js +441 -0
  6. package/dist/HomeKitHistory.js +2835 -0
  7. package/dist/camera.js +1276 -0
  8. package/dist/doorbell.js +122 -0
  9. package/dist/index.js +35 -0
  10. package/dist/nexustalk.js +741 -0
  11. package/dist/protect.js +240 -0
  12. package/dist/protobuf/google/rpc/status.proto +91 -0
  13. package/dist/protobuf/google/rpc/stream_body.proto +26 -0
  14. package/dist/protobuf/google/trait/product/camera.proto +53 -0
  15. package/dist/protobuf/googlehome/foyer.proto +208 -0
  16. package/dist/protobuf/nest/messages.proto +8 -0
  17. package/dist/protobuf/nest/services/apigateway.proto +107 -0
  18. package/dist/protobuf/nest/trait/audio.proto +7 -0
  19. package/dist/protobuf/nest/trait/cellular.proto +313 -0
  20. package/dist/protobuf/nest/trait/debug.proto +37 -0
  21. package/dist/protobuf/nest/trait/detector.proto +41 -0
  22. package/dist/protobuf/nest/trait/diagnostics.proto +87 -0
  23. package/dist/protobuf/nest/trait/firmware.proto +221 -0
  24. package/dist/protobuf/nest/trait/guest.proto +105 -0
  25. package/dist/protobuf/nest/trait/history.proto +345 -0
  26. package/dist/protobuf/nest/trait/humanlibrary.proto +19 -0
  27. package/dist/protobuf/nest/trait/hvac.proto +1353 -0
  28. package/dist/protobuf/nest/trait/input.proto +29 -0
  29. package/dist/protobuf/nest/trait/lighting.proto +61 -0
  30. package/dist/protobuf/nest/trait/located.proto +193 -0
  31. package/dist/protobuf/nest/trait/media.proto +68 -0
  32. package/dist/protobuf/nest/trait/network.proto +352 -0
  33. package/dist/protobuf/nest/trait/occupancy.proto +373 -0
  34. package/dist/protobuf/nest/trait/olive.proto +15 -0
  35. package/dist/protobuf/nest/trait/pairing.proto +85 -0
  36. package/dist/protobuf/nest/trait/product/camera.proto +283 -0
  37. package/dist/protobuf/nest/trait/product/detect.proto +67 -0
  38. package/dist/protobuf/nest/trait/product/doorbell.proto +18 -0
  39. package/dist/protobuf/nest/trait/product/guard.proto +59 -0
  40. package/dist/protobuf/nest/trait/product/protect.proto +344 -0
  41. package/dist/protobuf/nest/trait/promonitoring.proto +14 -0
  42. package/dist/protobuf/nest/trait/resourcedirectory.proto +32 -0
  43. package/dist/protobuf/nest/trait/safety.proto +119 -0
  44. package/dist/protobuf/nest/trait/security.proto +516 -0
  45. package/dist/protobuf/nest/trait/selftest.proto +78 -0
  46. package/dist/protobuf/nest/trait/sensor.proto +291 -0
  47. package/dist/protobuf/nest/trait/service.proto +46 -0
  48. package/dist/protobuf/nest/trait/structure.proto +85 -0
  49. package/dist/protobuf/nest/trait/system.proto +51 -0
  50. package/dist/protobuf/nest/trait/test.proto +15 -0
  51. package/dist/protobuf/nest/trait/ui.proto +65 -0
  52. package/dist/protobuf/nest/trait/user.proto +98 -0
  53. package/dist/protobuf/nest/trait/voiceassistant.proto +30 -0
  54. package/dist/protobuf/nestlabs/eventingapi/v1.proto +83 -0
  55. package/dist/protobuf/nestlabs/gateway/v1.proto +273 -0
  56. package/dist/protobuf/nestlabs/gateway/v2.proto +96 -0
  57. package/dist/protobuf/nestlabs/history/v1.proto +73 -0
  58. package/dist/protobuf/root.proto +64 -0
  59. package/dist/protobuf/wdl-event-importance.proto +11 -0
  60. package/dist/protobuf/wdl.proto +450 -0
  61. package/dist/protobuf/weave/common.proto +144 -0
  62. package/dist/protobuf/weave/trait/audio.proto +12 -0
  63. package/dist/protobuf/weave/trait/auth.proto +22 -0
  64. package/dist/protobuf/weave/trait/description.proto +32 -0
  65. package/dist/protobuf/weave/trait/heartbeat.proto +38 -0
  66. package/dist/protobuf/weave/trait/locale.proto +20 -0
  67. package/dist/protobuf/weave/trait/network.proto +24 -0
  68. package/dist/protobuf/weave/trait/pairing.proto +8 -0
  69. package/dist/protobuf/weave/trait/peerdevices.proto +18 -0
  70. package/dist/protobuf/weave/trait/power.proto +86 -0
  71. package/dist/protobuf/weave/trait/schedule.proto +76 -0
  72. package/dist/protobuf/weave/trait/security.proto +343 -0
  73. package/dist/protobuf/weave/trait/telemetry/tunnel.proto +37 -0
  74. package/dist/protobuf/weave/trait/time.proto +16 -0
  75. package/dist/res/Nest_camera_connecting.h264 +0 -0
  76. package/dist/res/Nest_camera_connecting.jpg +0 -0
  77. package/dist/res/Nest_camera_off.h264 +0 -0
  78. package/dist/res/Nest_camera_off.jpg +0 -0
  79. package/dist/res/Nest_camera_offline.h264 +0 -0
  80. package/dist/res/Nest_camera_offline.jpg +0 -0
  81. package/dist/res/Nest_camera_transfer.jpg +0 -0
  82. package/dist/streamer.js +344 -0
  83. package/dist/system.js +3112 -0
  84. package/dist/tempsensor.js +99 -0
  85. package/dist/thermostat.js +1026 -0
  86. package/dist/weather.js +205 -0
  87. package/dist/webrtc.js +55 -0
  88. package/package.json +66 -0
@@ -0,0 +1,1026 @@
1
+ // Nest Thermostat
2
+ // Part of homebridge-nest-accfactory
3
+ //
4
+ // Code version 3/9/2024
5
+ // Mark Hulskamp
6
+ 'use strict';
7
+
8
+ // Define our modules
9
+ import HomeKitDevice from './HomeKitDevice.js';
10
+
11
+ // Define nodejs module requirements
12
+ import path from 'node:path';
13
+
14
+ const LOWBATTERYLEVEL = 10; // Low battery level percentage
15
+ const MIN_TEMPERATURE = 9; // Minimum temperature for Nest Thermostat
16
+ const MAX_TEMPERATURE = 32; // Maximum temperature for Nest Thermostat
17
+
18
+ export default class NestThermostat extends HomeKitDevice {
19
+ batteryService = undefined;
20
+ occupancyService = undefined;
21
+ humidityService = undefined;
22
+ fanService = undefined;
23
+ dehumidifierService = undefined;
24
+ externalCool = undefined; // External module function
25
+ externalHeat = undefined; // External module function
26
+ externalFan = undefined; // External module function
27
+ externalDehumidifier = undefined; // External module function
28
+
29
+ constructor(accessory, api, log, eventEmitter, deviceData) {
30
+ super(accessory, api, log, eventEmitter, deviceData);
31
+ }
32
+
33
+ // Class functions
34
+ async addServices() {
35
+ // 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
+ }
40
+ this.thermostatService.setPrimaryService();
41
+
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
+ }
68
+
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
+ }
76
+
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
+ }
88
+
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,
138
+ });
139
+
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);
158
+ });
159
+
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;
164
+ });
165
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetTemperature).onGet(() => {
166
+ return this.getTemperature(this.hap.Characteristic.TargetTemperature);
167
+ });
168
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature).onGet(() => {
169
+ return this.getTemperature(this.hap.Characteristic.CoolingThresholdTemperature);
170
+ });
171
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature).onGet(() => {
172
+ return this.getTemperature(this.hap.Characteristic.HeatingThresholdTemperature);
173
+ });
174
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).onGet(() => {
175
+ return this.getMode();
176
+ });
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;
181
+ });
182
+
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);
187
+ }
188
+ if (this.occupancyService.testCharacteristic(this.hap.Characteristic.StatusFault) === false) {
189
+ this.occupancyService.addCharacteristic(this.hap.Characteristic.StatusFault);
190
+ }
191
+ this.thermostatService.addLinkedService(this.occupancyService);
192
+
193
+ // Setup battery service if not already present on the accessory
194
+ this.batteryService = this.accessory.getService(this.hap.Service.Battery);
195
+ if (this.batteryService === undefined) {
196
+ this.batteryService = this.accessory.addService(this.hap.Service.Battery, '', 1);
197
+ }
198
+ this.batteryService.setHiddenService(true);
199
+ this.thermostatService.addLinkedService(this.batteryService);
200
+
201
+ // Setup fan service if supported by the thermostat and not already present on the accessory
202
+ this.fanService = this.accessory.getService(this.hap.Service.Fanv2);
203
+ if (this.deviceData?.has_fan === true) {
204
+ if (this.fanService === undefined) {
205
+ this.fanService = this.accessory.addService(this.hap.Service.Fanv2, '', 1);
206
+ }
207
+ this.thermostatService.addLinkedService(this.fanService);
208
+
209
+ this.fanService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => {
210
+ this.setFan(value);
211
+ });
212
+ this.fanService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => {
213
+ return this.deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE;
214
+ });
215
+ }
216
+ if (this.deviceData?.has_fan === false && this.fanService !== undefined) {
217
+ // No longer have a Fan configured and service present, so removed it
218
+ this.accessory.removeService(this.fanService);
219
+ this.fanService === undefined;
220
+ }
221
+
222
+ // Setup dehumifider service if supported by the thermostat and not already present on the accessory
223
+ this.dehumidifierService = this.accessory.getService(this.hap.Service.HumidifierDehumidifier);
224
+ if (this.deviceData?.has_dehumidifier === true) {
225
+ if (this.dehumidifierService === undefined) {
226
+ this.dehumidifierService = this.accessory.addService(this.hap.Service.HumidifierDehumidifier, '', 1);
227
+ }
228
+ this.thermostatService.addLinkedService(this.dehumidifierService);
229
+
230
+ this.dehumidifierService.getCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState).setProps({
231
+ validValues: [this.hap.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER],
232
+ });
233
+ this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => {
234
+ this.setDehumidifier(value);
235
+ });
236
+ this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => {
237
+ return this.deviceData.dehumidifier_state === true
238
+ ? this.hap.Characteristic.Active.ACTIVE
239
+ : this.hap.Characteristic.Active.INACTIVE;
240
+ });
241
+ }
242
+ if (this.deviceData?.has_dehumidifier === false && this.dehumidifierService !== undefined) {
243
+ // No longer have a dehumidifier configured and service present, so removed it
244
+ this.accessory.removeService(this.dehumidifierService);
245
+ this.dehumidifierService = undefined;
246
+ }
247
+
248
+ // Setup humdity service if configured to be seperate and not already present on the accessory
249
+ this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor);
250
+ if (this.deviceData?.humiditySensor === true) {
251
+ if (this.humidityService === undefined) {
252
+ this.humidityService = this.accessory.addService(this.hap.Service.HumiditySensor, '', 1);
253
+ }
254
+ this.thermostatService.addLinkedService(this.humidityService);
255
+ }
256
+ if (this.deviceData?.humiditySensor === false && this.humidityService !== undefined) {
257
+ // No longer have a seperate humidity sensor configure and service present, so removed it
258
+ this.accessory.removeService(this.humidityService);
259
+ this.humidityService = undefined;
260
+ }
261
+
262
+ // Setup linkage to EveHome app if configured todo so
263
+ if (
264
+ this.deviceData?.eveHistory === true &&
265
+ this.thermostatService !== undefined &&
266
+ typeof this.historyService?.linkToEveHome === 'function'
267
+ ) {
268
+ this.historyService.linkToEveHome(this.thermostatService, {
269
+ description: this.deviceData.description,
270
+ getcommand: this.#EveHomeGetcommand.bind(this),
271
+ setcommand: this.#EveHomeSetcommand.bind(this),
272
+ });
273
+ }
274
+
275
+ // Attempt to load any external modules for this thermostat
276
+ // We support external cool/heat/fan/dehumidifier module functions
277
+ // This is all undocumented on how to use, as its for my specific use case :-)
278
+ const loadExternalModule = async (module) => {
279
+ if (typeof module !== 'string' || module === '') {
280
+ return;
281
+ }
282
+
283
+ let loadedModule = undefined;
284
+ try {
285
+ let values = module.match(/('.*?'|[^' ]+)(?=\s* |\s*$)/g);
286
+ let script = path.resolve(values[0]); // external library name
287
+ let options = values.slice(1); // options to be passed into the external library
288
+ let externalModule = await import(script);
289
+ if (typeof externalModule?.default === 'function') {
290
+ loadedModule = externalModule.default(this.log, options);
291
+ }
292
+ // eslint-disable-next-line no-unused-vars
293
+ } catch (error) {
294
+ this?.log?.warn && this.log.warn('Failed to load specified external module for thermostat "%s"', module);
295
+ }
296
+
297
+ return loadedModule;
298
+ };
299
+
300
+ this.externalCool = await loadExternalModule(this.deviceData?.externalCool);
301
+ this.externalHeat = await loadExternalModule(this.deviceData?.externalHeat);
302
+ this.externalFan = await loadExternalModule(this.deviceData?.externalFan);
303
+ this.externalDehumidifier = await loadExternalModule(this.deviceData?.externalDehumidifier);
304
+
305
+ // Create extra details for output
306
+ let postSetupDetails = [];
307
+ this.humidityService !== undefined && postSetupDetails.push('Seperate humidity sensor');
308
+ this.externalCool !== undefined && postSetupDetails.push('Using external cooling module');
309
+ this.externalHeat !== undefined && postSetupDetails.push('Using external heating module');
310
+ this.externalFan !== undefined && postSetupDetails.push('Using external fan module');
311
+ this.externalDehumidifier !== undefined && postSetupDetails.push('Using external dehumidification module');
312
+
313
+ return postSetupDetails;
314
+ }
315
+
316
+ setFan(fanState) {
317
+ this.set({ fan_state: fanState === this.hap.Characteristic.Active.ACTIVE ? true : false });
318
+ this.fanService.updateCharacteristic(this.hap.Characteristic.Active, fanState);
319
+
320
+ this?.log?.info &&
321
+ this.log.info(
322
+ 'Set fan on thermostat "%s" to "%s"',
323
+ this.deviceData.description,
324
+ fanState === this.hap.Characteristic.Active.ACTIVE ? 'On with initial fan speed of ' + this.deviceData.fan_current_speed : 'Off',
325
+ );
326
+ }
327
+
328
+ setDehumidifier(dehumidiferState) {
329
+ this.set({ dehumidifer_State: dehumidiferState === this.hap.Characteristic.Active.ACTIVE ? true : false });
330
+ this.dehumidifierService.updateCharacteristic(this.hap.Characteristic.Active, dehumidiferState);
331
+
332
+ this?.log?.info &&
333
+ this.log.info(
334
+ 'Set dehumidifer on thermostat "%s" to "%s"',
335
+ this.deviceData.description,
336
+ dehumidiferState === this.hap.Characteristic.Active.ACTIVE
337
+ ? 'On with target humidity level of ' + this.deviceData.target_humidity + '%'
338
+ : 'Off',
339
+ );
340
+ }
341
+
342
+ setDisplayUnit(temperatureUnit) {
343
+ this.set({ temperature_scale: temperatureUnit === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? 'C' : 'F' });
344
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, temperatureUnit);
345
+
346
+ this?.log?.info &&
347
+ this.log.info(
348
+ 'Set temperature units on thermostat "%s" to "%s"',
349
+ this.deviceData.description,
350
+ temperatureUnit === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? '°C' : '°F',
351
+ );
352
+ }
353
+
354
+ setMode(thermostatMode) {
355
+ if (thermostatMode !== this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value) {
356
+ // Work out based on the HomeKit requested mode, what can the thermostat really switch too
357
+ // We may over-ride the requested HomeKit mode
358
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.HEAT && this.deviceData.can_heat === false) {
359
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
360
+ }
361
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.COOL && this.deviceData.can_cool === false) {
362
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
363
+ }
364
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) {
365
+ // Workaround for 'Hey Siri, turn on my thermostat'
366
+ // Appears to automatically request mode as 'auto', but we need to see what Nest device supports
367
+ if (this.deviceData.can_cool === true && this.deviceData.can_heat === false) {
368
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.COOL;
369
+ }
370
+ if (this.deviceData.can_cool === false && this.deviceData.can_heat === true) {
371
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.HEAT;
372
+ }
373
+ if (this.deviceData.can_cool === false && this.deviceData.can_heat === false) {
374
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
375
+ }
376
+ }
377
+
378
+ let mode = '';
379
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.OFF) {
380
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature);
381
+ this.thermostatService.updateCharacteristic(
382
+ this.hap.Characteristic.TargetHeatingCoolingState,
383
+ this.hap.Characteristic.TargetHeatingCoolingState.OFF,
384
+ );
385
+ mode = 'off';
386
+ }
387
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.COOL) {
388
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature_high);
389
+ this.thermostatService.updateCharacteristic(
390
+ this.hap.Characteristic.TargetHeatingCoolingState,
391
+ this.hap.Characteristic.TargetHeatingCoolingState.COOL,
392
+ );
393
+ mode = 'cool';
394
+ }
395
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) {
396
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature_low);
397
+ this.thermostatService.updateCharacteristic(
398
+ this.hap.Characteristic.TargetHeatingCoolingState,
399
+ this.hap.Characteristic.TargetHeatingCoolingState.HEAT,
400
+ );
401
+ mode = 'heat';
402
+ }
403
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) {
404
+ this.thermostatService.updateCharacteristic(
405
+ this.hap.Characteristic.TargetTemperature,
406
+ (this.deviceData.target_temperature_low + this.deviceData.target_temperature_high) * 0.5,
407
+ );
408
+ this.thermostatService.updateCharacteristic(
409
+ this.hap.Characteristic.TargetHeatingCoolingState,
410
+ this.hap.Characteristic.TargetHeatingCoolingState.AUTO,
411
+ );
412
+ mode = 'range';
413
+ }
414
+
415
+ this.set({ hvac_mode: mode });
416
+
417
+ this?.log?.info && this.log.info('Set mode on "%s" to "%s"', this.deviceData.description, mode);
418
+ }
419
+ }
420
+
421
+ getMode() {
422
+ let currentMode = null;
423
+
424
+ if (this.deviceData.hvac_mode.toUpperCase() === 'HEAT' || this.deviceData.hvac_mode.toUpperCase() === 'ECOHEAT') {
425
+ // heating mode, either eco or normal;
426
+ currentMode = this.hap.Characteristic.TargetHeatingCoolingState.HEAT;
427
+ }
428
+ if (this.deviceData.hvac_mode.toUpperCase() === 'COOL' || this.deviceData.hvac_mode.toUpperCase() === 'ECOCOOL') {
429
+ // cooling mode, either eco or normal
430
+ currentMode = this.hap.Characteristic.TargetHeatingCoolingState.COOL;
431
+ }
432
+ if (this.deviceData.hvac_mode.toUpperCase() === 'RANGE' || this.deviceData.hvac_mode.toUpperCase() === 'ECORANGE') {
433
+ // range mode, either eco or normal
434
+ currentMode = this.hap.Characteristic.TargetHeatingCoolingState.AUTO;
435
+ }
436
+ if (this.deviceData.hvac_mode.toUpperCase() === 'OFF' || (this.deviceData.can_cool === false && this.deviceData.can_heat === false)) {
437
+ // off mode or no heating or cooling capability
438
+ currentMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
439
+ }
440
+
441
+ return currentMode;
442
+ }
443
+
444
+ setTemperature(characteristic, temperature) {
445
+ if (typeof characteristic === 'function' && typeof characteristic?.UUID === 'string') {
446
+ if (
447
+ characteristic.UUID === this.hap.Characteristic.TargetTemperature.UUID &&
448
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value !==
449
+ this.hap.Characteristic.TargetHeatingCoolingState.AUTO
450
+ ) {
451
+ this.set({ target_temperature: temperature });
452
+
453
+ this?.log?.info &&
454
+ this.log.info(
455
+ 'Set %s%s temperature on "%s" to "%s °C"',
456
+ this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '',
457
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value ===
458
+ this.hap.Characteristic.TargetHeatingCoolingState.HEAT
459
+ ? 'heating'
460
+ : 'cooling',
461
+ this.deviceData.description,
462
+ temperature,
463
+ );
464
+ }
465
+ if (
466
+ characteristic.UUID === this.hap.Characteristic.HeatingThresholdTemperature.UUID &&
467
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value ===
468
+ this.hap.Characteristic.TargetHeatingCoolingState.AUTO
469
+ ) {
470
+ this.set({ target_temperature_low: temperature });
471
+
472
+ this?.log?.info &&
473
+ this.log.info(
474
+ 'Set %sheating temperature on "%s" to "%s °C"',
475
+ this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '',
476
+ this.deviceData.description,
477
+ temperature,
478
+ );
479
+ }
480
+ if (
481
+ characteristic.UUID === this.hap.Characteristic.CoolingThresholdTemperature.UUID &&
482
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value ===
483
+ this.hap.Characteristic.TargetHeatingCoolingState.AUTO
484
+ ) {
485
+ this.set({ target_temperature_high: temperature });
486
+
487
+ this?.log?.info &&
488
+ this.log.info(
489
+ 'Set %scooling temperature on "%s" to "%s °C"',
490
+ this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '',
491
+ this.deviceData.description,
492
+ temperature,
493
+ );
494
+ }
495
+
496
+ this.thermostatService.updateCharacteristic(characteristic, temperature); // Update HomeKit with value
497
+ }
498
+ }
499
+
500
+ getTemperature(characteristic) {
501
+ let currentTemperature = null;
502
+
503
+ if (typeof characteristic === 'function' && typeof characteristic?.UUID === 'string') {
504
+ if (characteristic.UUID === this.hap.Characteristic.TargetTemperature.UUID) {
505
+ currentTemperature = this.deviceData.target_temperature;
506
+ }
507
+ if (characteristic.UUID === this.hap.Characteristic.HeatingThresholdTemperature.UUID) {
508
+ currentTemperature = this.deviceData.target_temperature_low;
509
+ }
510
+ if (characteristic.UUID === this.hap.Characteristic.CoolingThresholdTemperature.UUID) {
511
+ currentTemperature = this.deviceData.target_temperature_high;
512
+ }
513
+ if (currentTemperature < MIN_TEMPERATURE) {
514
+ currentTemperature = MIN_TEMPERATURE;
515
+ }
516
+ if (currentTemperature > MAX_TEMPERATURE) {
517
+ currentTemperature = MAX_TEMPERATURE;
518
+ }
519
+ }
520
+
521
+ return currentTemperature;
522
+ }
523
+
524
+ setChildlock(pin, value) {
525
+ // TODO - pincode setting when turning on.
526
+ // On REST API, writes to device.xxxxxxxx.temperature_lock_pin_hash. How is the hash calculated???
527
+ // Do we set temperature range limits when child lock on??
528
+
529
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.LockPhysicalControls, value); // Update HomeKit with value
530
+ if (value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED) {
531
+ // Set pin hash????
532
+ }
533
+ if (value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED) {
534
+ // Clear pin hash????
535
+ }
536
+ this.set({ temperature_lock: value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? true : false });
537
+
538
+ this?.log?.info &&
539
+ this.log.info(
540
+ 'Setting Childlock on "%s" to "%s"',
541
+ this.deviceData.description,
542
+ value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? 'Enabled' : 'Disabled',
543
+ );
544
+ }
545
+
546
+ updateServices(deviceData) {
547
+ if (
548
+ typeof deviceData !== 'object' ||
549
+ this.thermostatService === undefined ||
550
+ this.batteryService === undefined ||
551
+ this.occupancyService === undefined
552
+ ) {
553
+ return;
554
+ }
555
+
556
+ let historyEntry = {};
557
+
558
+ this.thermostatService.updateCharacteristic(
559
+ this.hap.Characteristic.TemperatureDisplayUnits,
560
+ deviceData.temperature_scale.toUpperCase() === 'C'
561
+ ? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS
562
+ : this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT,
563
+ );
564
+
565
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, deviceData.current_temperature);
566
+
567
+ this.thermostatService.updateCharacteristic(
568
+ this.hap.Characteristic.StatusFault,
569
+ deviceData.online === true && deviceData.removed_from_base === false
570
+ ? this.hap.Characteristic.StatusFault.NO_FAULT
571
+ : this.hap.Characteristic.StatusFault.GENERAL_FAULT,
572
+ ); // If Nest isn't online or removed from base, report in HomeKit
573
+
574
+ this.thermostatService.updateCharacteristic(
575
+ this.hap.Characteristic.LockPhysicalControls,
576
+ deviceData.temperature_lock === true
577
+ ? this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED
578
+ : this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED,
579
+ );
580
+
581
+ // Update air filter sttaus if has been added
582
+ if (this.thermostatService.testCharacteristic(this.hap.Characteristic.FilterChangeIndication) === true) {
583
+ this.thermostatService.updateCharacteristic(
584
+ this.hap.Characteristic.FilterChangeIndication,
585
+ deviceData.has_air_filter && deviceData.filter_replacement_needed === true
586
+ ? this.hap.Characteristic.FilterChangeIndication.CHANGE_FILTER
587
+ : this.hap.Characteristic.FilterChangeIndication.FILTER_OK,
588
+ );
589
+ }
590
+
591
+ // Using a temperature sensor as active temperature?
592
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.StatusActive, deviceData.active_rcs_sensor === '');
593
+
594
+ // Update battery status
595
+ this.batteryService.updateCharacteristic(this.hap.Characteristic.BatteryLevel, deviceData.battery_level);
596
+ this.batteryService.updateCharacteristic(
597
+ this.hap.Characteristic.StatusLowBattery,
598
+ deviceData.battery_level > LOWBATTERYLEVEL
599
+ ? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
600
+ : this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW,
601
+ );
602
+ this.batteryService.updateCharacteristic(
603
+ this.hap.Characteristic.ChargingState,
604
+ (deviceData.battery_level > this.deviceData.battery_level && this.deviceData.battery_level !== 0 ? true : false)
605
+ ? this.hap.Characteristic.ChargingState.CHARGING
606
+ : this.hap.Characteristic.ChargingState.NOT_CHARGING,
607
+ );
608
+
609
+ // Update for away/home status. Away = no occupancy detected, Home = Occupancy Detected
610
+ this.occupancyService.updateCharacteristic(
611
+ this.hap.Characteristic.OccupancyDetected,
612
+ deviceData.occupancy === true
613
+ ? this.hap.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED
614
+ : this.hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED,
615
+ );
616
+ this.occupancyService.updateCharacteristic(
617
+ this.hap.Characteristic.StatusFault,
618
+ deviceData.online === true && deviceData.removed_from_base === false
619
+ ? this.hap.Characteristic.StatusFault.NO_FAULT
620
+ : this.hap.Characteristic.StatusFault.GENERAL_FAULT,
621
+ ); // If Nest isn't online or removed from base, report in HomeKit
622
+
623
+ // Update seperate humidity sensor if configured todo so
624
+ if (this.humidityService !== undefined) {
625
+ this.humidityService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, deviceData.current_humidity);
626
+ this.humidityService.updateCharacteristic(
627
+ this.hap.Characteristic.StatusFault,
628
+ deviceData.online === true && deviceData.removed_from_base === false
629
+ ? this.hap.Characteristic.StatusFault.NO_FAULT
630
+ : this.hap.Characteristic.StatusFault.GENERAL_FAULT,
631
+ ); // If Nest isn't online or removed from base, report in HomeKit
632
+ }
633
+
634
+ // Update humity on thermostat
635
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, deviceData.current_humidity);
636
+
637
+ // Check for fan setup change on thermostat
638
+ if (deviceData.has_fan !== this.deviceData.has_fan) {
639
+ if (deviceData.has_fan === true && this.deviceData.has_fan === false && this.fanService === undefined) {
640
+ // Fan has been added
641
+ this.fanService = this.accessory.addService(this.hap.Service.Fanv2, '', 1);
642
+ this.thermostatService.addLinkedService(this.fanService);
643
+
644
+ this.fanService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => {
645
+ this.setFan(value);
646
+ });
647
+
648
+ this.fanService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => {
649
+ return this.deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE;
650
+ });
651
+ }
652
+ if (deviceData.has_fan === false && this.deviceData.has_fan === true && this.fanService !== undefined) {
653
+ // Fan has been removed
654
+ this.accessory.removeService(this.fanService);
655
+ this.fanService = undefined;
656
+ }
657
+
658
+ this?.log?.info &&
659
+ this.log.info(
660
+ 'Fan setup on thermostat "%s" has changed. Fan was',
661
+ this.deviceData.description,
662
+ this.fanService === undefined ? 'removed' : 'added',
663
+ );
664
+ }
665
+
666
+ // Check for dehumidifer setup change on thermostat
667
+ if (deviceData.has_dehumidifier !== this.deviceData.has_dehumidifier) {
668
+ if (deviceData.has_dehumidifier === true && this.deviceData.has_dehumidifier === false && this.dehumidifierService === undefined) {
669
+ // Dehumidifier has been added
670
+ this.dehumidifierService = this.accessory.addService(this.hap.Service.HumidifierDehumidifier, '', 1);
671
+ this.thermostatService.addLinkedService(this.dehumidifierService);
672
+
673
+ this.dehumidifierService.getCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState).setProps({
674
+ validValues: [this.hap.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER],
675
+ });
676
+
677
+ this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => {
678
+ this.setDehumidifier(value);
679
+ });
680
+
681
+ this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => {
682
+ return this.deviceData.dehumidifier_state === true
683
+ ? this.hap.Characteristic.Active.ACTIVE
684
+ : this.hap.Characteristic.Active.INACTIVE;
685
+ });
686
+ }
687
+ if (deviceData.has_dehumidifier === false && this.deviceData.has_dehumidifier === true && this.dehumidifierService !== undefined) {
688
+ // Dehumidifer has been removed
689
+ this.accessory.removeService(this.dehumidifierService);
690
+ this.dehumidifierService = undefined;
691
+ }
692
+
693
+ this?.log?.info &&
694
+ this.log.info(
695
+ 'Dehumidifier setup on thermostat "%s" has changed. Dehumidifier was',
696
+ this.deviceData.description,
697
+ this.dehumidifierService === undefined ? 'removed' : 'added',
698
+ );
699
+ }
700
+
701
+ if (deviceData.can_cool !== this.deviceData.can_cool || deviceData.can_heat !== this.deviceData.can_heat) {
702
+ // Heating and/cooling setup has changed on thermostat
703
+
704
+ // Limit prop ranges
705
+ if (deviceData.can_cool === false && deviceData.can_heat === true) {
706
+ // Can heat only, so set values allowed for mode off/heat
707
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({
708
+ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.HEAT],
709
+ });
710
+ }
711
+ if (deviceData.can_cool === true && deviceData.can_heat === false) {
712
+ // Can cool only
713
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({
714
+ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.COOL],
715
+ });
716
+ }
717
+ if (deviceData.can_cool === true && deviceData.can_heat === true) {
718
+ // heat and cool
719
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({
720
+ validValues: [
721
+ this.hap.Characteristic.TargetHeatingCoolingState.OFF,
722
+ this.hap.Characteristic.TargetHeatingCoolingState.HEAT,
723
+ this.hap.Characteristic.TargetHeatingCoolingState.COOL,
724
+ this.hap.Characteristic.TargetHeatingCoolingState.AUTO,
725
+ ],
726
+ });
727
+ }
728
+ if (deviceData.can_cool === false && deviceData.can_heat === false) {
729
+ // only off mode
730
+ this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({
731
+ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF],
732
+ });
733
+ }
734
+
735
+ this?.log?.info && this.log.info('Heating/cooling setup on thermostat on "%s" has changed', this.deviceData.description);
736
+ }
737
+
738
+ // Update current mode temperatures
739
+ if (
740
+ deviceData.can_heat === true &&
741
+ (deviceData.hvac_mode.toUpperCase() === 'HEAT' || deviceData.hvac_mode.toUpperCase() === 'ECOHEAT')
742
+ ) {
743
+ // heating mode, either eco or normal
744
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, deviceData.target_temperature_low);
745
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.target_temperature_low);
746
+ this.thermostatService.updateCharacteristic(
747
+ this.hap.Characteristic.TargetHeatingCoolingState,
748
+ this.hap.Characteristic.TargetHeatingCoolingState.HEAT,
749
+ );
750
+ historyEntry.target = { low: 0, high: deviceData.target_temperature_low }; // single target temperature for heating limit
751
+ }
752
+ if (
753
+ deviceData.can_cool === true &&
754
+ (deviceData.hvac_mode.toUpperCase() === 'COOL' || deviceData.hvac_mode.toUpperCase() === 'ECOCOOL')
755
+ ) {
756
+ // cooling mode, either eco or normal
757
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, deviceData.target_temperature_high);
758
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.target_temperature_high);
759
+ this.thermostatService.updateCharacteristic(
760
+ this.hap.Characteristic.TargetHeatingCoolingState,
761
+ this.hap.Characteristic.TargetHeatingCoolingState.COOL,
762
+ );
763
+ historyEntry.target = { low: deviceData.target_temperature_high, high: 0 }; // single target temperature for cooling limit
764
+ }
765
+ if (
766
+ deviceData.can_cool === true &&
767
+ deviceData.can_heat === true &&
768
+ (deviceData.hvac_mode.toUpperCase() === 'RANGE' || deviceData.hvac_mode.toUpperCase() === 'ECORANGE')
769
+ ) {
770
+ // range mode, either eco or normal
771
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, deviceData.target_temperature_low);
772
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, deviceData.target_temperature_high);
773
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.target_temperature);
774
+ this.thermostatService.updateCharacteristic(
775
+ this.hap.Characteristic.TargetHeatingCoolingState,
776
+ this.hap.Characteristic.TargetHeatingCoolingState.AUTO,
777
+ );
778
+ historyEntry.target = { low: deviceData.target_temperature_low, high: deviceData.target_temperature_high };
779
+ }
780
+ if (deviceData.can_cool === false && deviceData.can_heat === false && deviceData.hvac_mode.toUpperCase() === 'OFF') {
781
+ // off mode.
782
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.target_temperature);
783
+ this.thermostatService.updateCharacteristic(
784
+ this.hap.Characteristic.TargetHeatingCoolingState,
785
+ this.hap.Characteristic.TargetHeatingCoolingState.OFF,
786
+ );
787
+ historyEntry.target = { low: 0, high: 0 }; // thermostat off, so no target temperatures
788
+ }
789
+
790
+ // Update current state
791
+ if (deviceData.hvac_state.toUpperCase() === 'HEATING') {
792
+ if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && this.externalCool !== undefined) {
793
+ // Switched to heating mode and external cooling external code was being used, so stop cooling via cooling external code
794
+ if (typeof this.externalCool.off === 'function') {
795
+ this.externalCool.off();
796
+ }
797
+ }
798
+ if (
799
+ (this.deviceData.hvac_state.toUpperCase() !== 'HEATING' ||
800
+ deviceData.target_temperature_low !== this.deviceData.target_temperature_low) &&
801
+ this.externalHeat !== undefined
802
+ ) {
803
+ // Switched to heating mode and external heating external code is being used
804
+ // Start heating via heating external code OR adjust heating target temperature due to change
805
+ if (typeof this.externalHeat.heat === 'function') {
806
+ this.externalHeat.heat(deviceData.deviceData.target_temperature_low);
807
+ }
808
+ }
809
+ this.thermostatService.updateCharacteristic(
810
+ this.hap.Characteristic.CurrentHeatingCoolingState,
811
+ this.hap.Characteristic.CurrentHeatingCoolingState.HEAT,
812
+ );
813
+ historyEntry.status = 2; // heating
814
+ }
815
+ if (deviceData.hvac_state.toUpperCase() === 'COOLING') {
816
+ if (this.deviceData.hvac_state.toUpperCase() === 'HEATING' && this.externalHeat !== undefined) {
817
+ // Switched to cooling mode and external heating external code was being used, so stop heating via heating external code
818
+ if (typeof this.externalHeat.off === 'function') {
819
+ this.externalHeat.off();
820
+ }
821
+ }
822
+ if (
823
+ (this.deviceData.hvac_state.toUpperCase() !== 'COOLING' ||
824
+ deviceData.target_temperature_high !== this.deviceData.target_temperature_high) &&
825
+ this.externalCool !== undefined
826
+ ) {
827
+ // Switched to cooling mode and external cooling external code is being used
828
+ // Start cooling via cooling external code OR adjust cooling target temperature due to change
829
+ if (typeof this.externalCool.cool === 'function') {
830
+ this.externalCool.cool(deviceData.target_temperature_high);
831
+ }
832
+ }
833
+ this.thermostatService.updateCharacteristic(
834
+ this.hap.Characteristic.CurrentHeatingCoolingState,
835
+ this.hap.Characteristic.CurrentHeatingCoolingState.COOL,
836
+ );
837
+ historyEntry.status = 3; // cooling
838
+ }
839
+ if (deviceData.hvac_state.toUpperCase() === 'OFF') {
840
+ if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && this.externalCool !== undefined) {
841
+ // Switched to off mode and external cooling external code was being used, so stop cooling via cooling external code
842
+ if (typeof this.externalCool.off === 'function') {
843
+ this.externalCool.off();
844
+ }
845
+ }
846
+ if (this.deviceData.hvac_state.toUpperCase() === 'HEATING' && this.externalHeat !== undefined) {
847
+ // Switched to off mode and external heating external code was being used, so stop heating via heating external code
848
+ if (typeof this.externalHeat.heat === 'function') {
849
+ this.externalHeat.off();
850
+ }
851
+ }
852
+ this.thermostatService.updateCharacteristic(
853
+ this.hap.Characteristic.CurrentHeatingCoolingState,
854
+ this.hap.Characteristic.CurrentHeatingCoolingState.OFF,
855
+ );
856
+ historyEntry.status = 0; // off
857
+ }
858
+ if (this.fanService !== undefined) {
859
+ if (this.deviceData.fan_state === false && deviceData.fan_state === true && this.externalFan !== undefined) {
860
+ // Fan mode was switched on and external fan external code is being used, so start fan via fan external code
861
+ if (typeof this.externalFan.fan === 'function') {
862
+ this.externalFan.fan(0); // Fan speed will be auto
863
+ }
864
+ }
865
+ if (this.deviceData.fan_state === true && deviceData.fan_state === false && this.externalFan !== undefined) {
866
+ // Fan mode was switched off and external fan external code was being used, so stop fan via fan external code
867
+ if (typeof this.externalFan.off === 'function') {
868
+ this.externalFan.off();
869
+ }
870
+ }
871
+ //this.fanService.updateCharacteristic(this.hap.Characteristic.RotationSpeed,
872
+ // (deviceData.fan_state === true ? (deviceData.fan_current_speed / deviceData.fan_max_speed) * 100 : 0));
873
+ this.fanService.updateCharacteristic(
874
+ this.hap.Characteristic.Active,
875
+ deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
876
+ ); // fan status on or off
877
+ historyEntry.status = 1; // fan
878
+ }
879
+ if (this.dehumidifierService !== undefined) {
880
+ if (
881
+ this.deviceData.dehumidifier_state === false &&
882
+ deviceData.dehumidifier_state === true &&
883
+ this.externalDehumidifier !== undefined
884
+ ) {
885
+ // Dehumidifier mode was switched on and external dehumidifier external code is being used
886
+ // Start dehumidifier via dehumidifiern external code
887
+ if (typeof this.externalDehumidifier.dehumififier === 'function') {
888
+ this.externalDehumidifier.dehumififier(0);
889
+ }
890
+ }
891
+ if (
892
+ this.deviceData.dehumidifier_state === true &&
893
+ deviceData.dehumidifier_state === false &&
894
+ this.externalDehumidifier !== undefined
895
+ ) {
896
+ // Dehumidifier mode was switched off and external dehumidifier external code was being used
897
+ // Stop dehumidifier via dehumidifier external code
898
+ if (typeof this.externalDehumidifier.off === 'function') {
899
+ this.externalDehumidifier.off();
900
+ }
901
+ }
902
+
903
+ this.dehumidifierService.updateCharacteristic(
904
+ this.hap.Characteristic.Active,
905
+ deviceData.dehumidifier_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
906
+ ); // dehumidifier status on or off
907
+ historyEntry.status = 4; // dehumidifier
908
+ }
909
+
910
+ // Log thermostat metrics to history only if changed to previous recording
911
+ if (this.thermostatService !== undefined && typeof this.historyService?.addHistory === 'function') {
912
+ let tempEntry = this.historyService.lastHistory(this.thermostatService);
913
+ if (
914
+ tempEntry === null ||
915
+ (typeof tempEntry === 'object' && tempEntry.status !== historyEntry.status) ||
916
+ tempEntry.temperature !== deviceData.current_temperature ||
917
+ JSON.stringify(tempEntry.target) !== JSON.stringify(historyEntry.target) ||
918
+ tempEntry.humidity !== deviceData.current_humidity
919
+ ) {
920
+ this.historyService.addHistory(this.thermostatService, {
921
+ time: Math.floor(Date.now() / 1000),
922
+ status: historyEntry.status,
923
+ temperature: deviceData.current_temperature,
924
+ target: historyEntry.target,
925
+ humidity: deviceData.current_humidity,
926
+ });
927
+ }
928
+ }
929
+
930
+ // Notify Eve App of device status changes if linked
931
+ if (
932
+ this.deviceData.eveHistory === true &&
933
+ this.thermostatService !== undefined &&
934
+ typeof this.historyService?.updateEveHome === 'function'
935
+ ) {
936
+ // Update our internal data with properties Eve will need to process
937
+ this.deviceData.online = deviceData.online;
938
+ this.deviceData.removed_from_base = deviceData.removed_from_base;
939
+ this.deviceData.vacation_mode = deviceData.vacation_mode;
940
+ this.deviceData.hvac_mode = deviceData.hvac_mode;
941
+ this.deviceData.schedules = deviceData.schedules;
942
+ this.deviceData.schedule_mode = deviceData.schedule_mode;
943
+ this.historyService.updateEveHome(this.thermostatService, this.#EveHomeGetcommand.bind(this));
944
+ }
945
+ }
946
+
947
+ #EveHomeGetcommand(EveHomeGetData) {
948
+ // Pass back extra data for Eve Thermo onGet() to process command
949
+ // Data will already be an object, our only job is to add/modify it
950
+ if (typeof EveHomeGetData === 'object') {
951
+ EveHomeGetData.enableschedule = this.deviceData.schedule_mode === 'heat'; // Schedules on/off
952
+ EveHomeGetData.attached = this.deviceData.online === true && this.deviceData.removed_from_base === false;
953
+ EveHomeGetData.vacation = this.deviceData.vacation_mode === true; // Vaction mode on/off
954
+ EveHomeGetData.vacationtemp = this.deviceData.vacation_mode === true ? EveHomeGetData.vacationtemp : null;
955
+ EveHomeGetData.programs = []; // No programs yet, we'll process this below
956
+ if (this.deviceData.schedule_mode.toUpperCase() === 'HEAT' || this.deviceData.schedule_mode.toUpperCase() === 'RANGE') {
957
+ const DAYSOFWEEK = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
958
+
959
+ Object.entries(this.deviceData.schedules).forEach(([day, schedules]) => {
960
+ let tempSchedule = [];
961
+ let tempTemperatures = [];
962
+ Object.values(schedules)
963
+ .reverse()
964
+ .forEach((schedule) => {
965
+ if (schedule.entry_type === 'setpoint' && (schedule.type === 'HEAT' || schedule.type === 'RANGE')) {
966
+ tempSchedule.push({
967
+ start: schedule.time,
968
+ duration: 0,
969
+ temperature: typeof schedule['temp-min'] === 'number' ? schedule['temp-min'] : schedule.temp,
970
+ });
971
+ tempTemperatures.push(typeof schedule['temp-min'] === 'number' ? schedule['temp-min'] : schedule.temp);
972
+ }
973
+ });
974
+
975
+ // Sort the schedule array by start time
976
+ tempSchedule = tempSchedule.sort((a, b) => {
977
+ if (a.start < b.start) {
978
+ return -1;
979
+ }
980
+ });
981
+
982
+ let ecoTemp = tempTemperatures.length === 0 ? 0 : Math.min(...tempTemperatures);
983
+ let comfortTemp = tempTemperatures.length === 0 ? 0 : Math.max(...tempTemperatures);
984
+ let program = {};
985
+ program.id = parseInt(day) + 1;
986
+ program.days = DAYSOFWEEK[day];
987
+ program.schedule = [];
988
+ let lastTime = 86400; // seconds in a day
989
+ Object.values(tempSchedule)
990
+ .reverse()
991
+ .forEach((schedule) => {
992
+ if (schedule.temperature === comfortTemp) {
993
+ // We only want to add the schedule time if its using the 'max' temperature
994
+ program.schedule.push({
995
+ start: schedule.start,
996
+ duration: lastTime - schedule.start,
997
+ ecotemp: ecoTemp,
998
+ comforttemp: comfortTemp,
999
+ });
1000
+ }
1001
+ lastTime = schedule.start;
1002
+ });
1003
+ EveHomeGetData.programs.push(program);
1004
+ });
1005
+ }
1006
+ }
1007
+ return EveHomeGetData;
1008
+ }
1009
+
1010
+ #EveHomeSetcommand(EveHomeSetData) {
1011
+ if (typeof EveHomeSetData !== 'object') {
1012
+ return;
1013
+ }
1014
+
1015
+ if (typeof EveHomeSetData?.vacation === 'boolean') {
1016
+ this.deviceData.vacation_mode = EveHomeSetData.vacation.status;
1017
+ this.set({ vacation_mode: this.deviceData.vacation_mode });
1018
+ }
1019
+ if (typeof EveHomeSetData?.programs === 'object') {
1020
+ /* EveHomeSetData.programs.forEach((day) => {
1021
+ // Convert into Nest thermostat schedule format and set. Need to work this out
1022
+ //this.set({'days' : {6 : { 'temp' : 17 , 'time' : 13400, touched_at: Date.now()}} });
1023
+ }); */
1024
+ }
1025
+ }
1026
+ }