homebridge-nest-accfactory 0.3.1 → 0.3.2

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.
@@ -7,25 +7,32 @@
7
7
  // Define nodejs module requirements
8
8
  import path from 'node:path';
9
9
  import fs from 'node:fs';
10
- import { fileURLToPath } from 'node:url';
11
10
 
12
11
  // Define our modules
13
12
  import HomeKitDevice from '../HomeKitDevice.js';
13
+ import { processCommonData, scaleValue, adjustTemperature } from '../utils.js';
14
14
 
15
15
  // Define constants
16
- const LOW_BATTERY_LEVEL = 10; // Low battery level percentage
17
- const MIN_TEMPERATURE = 9; // Minimum temperature for Nest Thermostat
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
16
+ import {
17
+ DATA_SOURCE,
18
+ THERMOSTAT_MIN_TEMPERATURE,
19
+ THERMOSTAT_MAX_TEMPERATURE,
20
+ LOW_BATTERY_LEVEL,
21
+ PROTOBUF_RESOURCES,
22
+ DAYS_OF_WEEK_FULL,
23
+ DAYS_OF_WEEK_SHORT,
24
+ __dirname,
25
+ DEVICE_TYPE,
26
+ } from '../consts.js';
20
27
 
21
28
  export default class NestThermostat extends HomeKitDevice {
22
29
  static TYPE = 'Thermostat';
23
- static VERSION = '2025.06.15';
30
+ static VERSION = '2025.08.07'; // Code version
24
31
 
32
+ thermostatService = undefined;
25
33
  batteryService = undefined;
26
34
  occupancyService = undefined;
27
35
  humidityService = undefined;
28
- switchService = undefined; // Hotwater heating boost control
29
36
  fanService = undefined; // Fan control
30
37
  dehumidifierService = undefined; // dehumidifier (only) control
31
38
  externalCool = undefined; // External module function
@@ -33,18 +40,25 @@ export default class NestThermostat extends HomeKitDevice {
33
40
  externalFan = undefined; // External module function
34
41
  externalDehumidifier = undefined; // External module function
35
42
 
36
- constructor(accessory, api, log, eventEmitter, deviceData) {
37
- super(accessory, api, log, eventEmitter, deviceData);
38
- }
39
-
40
43
  // Class functions
41
- async setupDevice() {
42
- // Setup the thermostat service if not already present on the accessory
43
- this.thermostatService = this.addHKService(this.hap.Service.Thermostat, '', 1);
44
+ async onAdd() {
45
+ // Setup the thermostat service if not already present on the accessory, and link it to the Eve app if configured to do so
46
+ this.thermostatService = this.addHKService(this.hap.Service.Thermostat, '', 1, { messages: this.message.bind(this) });
44
47
  this.thermostatService.setPrimaryService();
45
48
 
46
49
  // Setup set characteristics
47
50
 
51
+ // Patch to avoid characteristic errors when setting initial property ranges
52
+ this.hap.Characteristic.TargetTemperature.prototype.getDefaultValue = () => {
53
+ return THERMOSTAT_MIN_TEMPERATURE; // start at minimum target temperature
54
+ };
55
+ this.hap.Characteristic.HeatingThresholdTemperature.prototype.getDefaultValue = () => {
56
+ return THERMOSTAT_MIN_TEMPERATURE; // start at minimum heating threshold
57
+ };
58
+ this.hap.Characteristic.CoolingThresholdTemperature.prototype.getDefaultValue = () => {
59
+ return THERMOSTAT_MAX_TEMPERATURE; // start at maximum cooling threshold
60
+ };
61
+
48
62
  // Used to indicate active temperature if the thermostat is using its temperature sensor data
49
63
  // or an external temperature sensor ie: Nest Temperature Sensor
50
64
  this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.StatusActive);
@@ -73,7 +87,7 @@ export default class NestThermostat extends HomeKitDevice {
73
87
  this.setDisplayUnit(value);
74
88
  },
75
89
  onGet: () => {
76
- return this.deviceData.temperature_scale === 'C'
90
+ return this.deviceData.temperature_scale.toUpperCase() === 'C'
77
91
  ? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS
78
92
  : this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT;
79
93
  },
@@ -87,6 +101,11 @@ export default class NestThermostat extends HomeKitDevice {
87
101
  });
88
102
 
89
103
  this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TargetTemperature, {
104
+ props: {
105
+ minStep: 0.5,
106
+ minValue: THERMOSTAT_MIN_TEMPERATURE,
107
+ maxValue: THERMOSTAT_MAX_TEMPERATURE,
108
+ },
90
109
  onSet: (value) => {
91
110
  this.setTemperature(this.hap.Characteristic.TargetTemperature, value);
92
111
  },
@@ -98,8 +117,8 @@ export default class NestThermostat extends HomeKitDevice {
98
117
  this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.CoolingThresholdTemperature, {
99
118
  props: {
100
119
  minStep: 0.5,
101
- minValue: MIN_TEMPERATURE,
102
- maxValue: MAX_TEMPERATURE,
120
+ minValue: THERMOSTAT_MIN_TEMPERATURE,
121
+ maxValue: THERMOSTAT_MAX_TEMPERATURE,
103
122
  },
104
123
  onSet: (value) => {
105
124
  this.setTemperature(this.hap.Characteristic.CoolingThresholdTemperature, value);
@@ -112,8 +131,8 @@ export default class NestThermostat extends HomeKitDevice {
112
131
  this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.HeatingThresholdTemperature, {
113
132
  props: {
114
133
  minStep: 0.5,
115
- minValue: MIN_TEMPERATURE,
116
- maxValue: MAX_TEMPERATURE,
134
+ minValue: THERMOSTAT_MIN_TEMPERATURE,
135
+ maxValue: THERMOSTAT_MAX_TEMPERATURE,
117
136
  },
118
137
  onSet: (value) => {
119
138
  this.setTemperature(this.hap.Characteristic.HeatingThresholdTemperature, value);
@@ -128,11 +147,11 @@ export default class NestThermostat extends HomeKitDevice {
128
147
  validValues:
129
148
  this.deviceData?.can_cool === true && this.deviceData?.can_heat === true
130
149
  ? [
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
- ]
150
+ this.hap.Characteristic.TargetHeatingCoolingState.OFF,
151
+ this.hap.Characteristic.TargetHeatingCoolingState.HEAT,
152
+ this.hap.Characteristic.TargetHeatingCoolingState.COOL,
153
+ this.hap.Characteristic.TargetHeatingCoolingState.AUTO,
154
+ ]
136
155
  : this.deviceData?.can_heat === true
137
156
  ? [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.HEAT]
138
157
  : this.deviceData?.can_cool === true
@@ -211,32 +230,6 @@ export default class NestThermostat extends HomeKitDevice {
211
230
  this.humidityService = undefined;
212
231
  }
213
232
 
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
-
227
- // Setup linkage to EveHome app if configured todo so
228
- if (
229
- this.deviceData?.eveHistory === true &&
230
- this.thermostatService !== undefined &&
231
- typeof this.historyService?.linkToEveHome === 'function'
232
- ) {
233
- this.historyService.linkToEveHome(this.thermostatService, {
234
- description: this.deviceData.description,
235
- getcommand: this.#EveHomeGetcommand.bind(this),
236
- setcommand: this.#EveHomeSetcommand.bind(this),
237
- });
238
- }
239
-
240
233
  // Attempt to load any external modules for this thermostat
241
234
  // We support external cool/heat/fan/dehumidifier module functions
242
235
  // This is all undocumented on how to use, as its for my specific use case :-)
@@ -253,263 +246,26 @@ export default class NestThermostat extends HomeKitDevice {
253
246
  this.externalDehumidifier !== undefined && this.postSetupDetail('Using external dehumidification module');
254
247
  }
255
248
 
256
- setFan(fanState, speed) {
257
- if (
258
- fanState !== this.fanService.getCharacteristic(this.hap.Characteristic.Active).value ||
259
- speed !== this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).value
260
- ) {
261
- this.set({
262
- uuid: this.deviceData.nest_google_uuid,
263
- fan_state: fanState === this.hap.Characteristic.Active.ACTIVE ? true : false,
264
- fan_timer_speed: Math.round((speed / 100) * this.deviceData.fan_max_speed),
265
- });
266
- this.fanService.updateCharacteristic(this.hap.Characteristic.Active, fanState);
267
- this.fanService.updateCharacteristic(this.hap.Characteristic.RotationSpeed, speed);
268
-
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
- );
274
- }
249
+ onRemove() {
250
+ this.accessory.removeService(this.thermostatService);
251
+ this.accessory.removeService(this.batteryService);
252
+ this.accessory.removeService(this.occupancyService);
253
+ this.accessory.removeService(this.humidityService);
254
+ this.accessory.removeService(this.fanService);
255
+ this.accessory.removeService(this.dehumidifierService);
256
+ this.thermostatService = undefined;
257
+ this.batteryService = undefined;
258
+ this.occupancyService = undefined;
259
+ this.humidityService = undefined;
260
+ this.fanService = undefined;
261
+ this.dehumidifierService = undefined;
262
+ this.externalCool = undefined;
263
+ this.externalHeat = undefined;
264
+ this.externalFan = undefined;
265
+ this.externalDehumidifier = undefined;
275
266
  }
276
267
 
277
- setDehumidifier(dehumidiferState) {
278
- this.set({
279
- uuid: this.deviceData.nest_google_uuid,
280
- dehumidifier_state: dehumidiferState === this.hap.Characteristic.Active.ACTIVE ? true : false,
281
- });
282
- this.dehumidifierService.updateCharacteristic(this.hap.Characteristic.Active, dehumidiferState);
283
-
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
- );
305
- }
306
-
307
- setDisplayUnit(temperatureUnit) {
308
- this.set({
309
- uuid: this.deviceData.nest_google_uuid,
310
- temperature_scale: temperatureUnit === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? 'C' : 'F',
311
- });
312
- this.thermostatService.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, temperatureUnit);
313
-
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
- );
319
- }
320
-
321
- setMode(thermostatMode) {
322
- if (thermostatMode !== this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value) {
323
- // Work out based on the HomeKit requested mode, what can the thermostat really switch too
324
- // We may over-ride the requested HomeKit mode
325
- if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.HEAT && this.deviceData.can_heat === false) {
326
- thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
327
- }
328
- if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.COOL && this.deviceData.can_cool === false) {
329
- thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
330
- }
331
- if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) {
332
- // Workaround for 'Hey Siri, turn on my thermostat'
333
- // Appears to automatically request mode as 'auto', but we need to see what Nest device supports
334
- if (this.deviceData.can_cool === true && this.deviceData.can_heat === false) {
335
- thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.COOL;
336
- }
337
- if (this.deviceData.can_cool === false && this.deviceData.can_heat === true) {
338
- thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.HEAT;
339
- }
340
- if (this.deviceData.can_cool === false && this.deviceData.can_heat === false) {
341
- thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
342
- }
343
- }
344
-
345
- let mode = '';
346
- if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.OFF) {
347
- this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature);
348
- this.thermostatService.updateCharacteristic(
349
- this.hap.Characteristic.TargetHeatingCoolingState,
350
- this.hap.Characteristic.TargetHeatingCoolingState.OFF,
351
- );
352
- mode = 'off';
353
- }
354
- if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.COOL) {
355
- this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature_high);
356
- this.thermostatService.updateCharacteristic(
357
- this.hap.Characteristic.TargetHeatingCoolingState,
358
- this.hap.Characteristic.TargetHeatingCoolingState.COOL,
359
- );
360
- mode = 'cool';
361
- }
362
- if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) {
363
- this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature_low);
364
- this.thermostatService.updateCharacteristic(
365
- this.hap.Characteristic.TargetHeatingCoolingState,
366
- this.hap.Characteristic.TargetHeatingCoolingState.HEAT,
367
- );
368
- mode = 'heat';
369
- }
370
- if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) {
371
- this.thermostatService.updateCharacteristic(
372
- this.hap.Characteristic.TargetTemperature,
373
- (this.deviceData.target_temperature_low + this.deviceData.target_temperature_high) * 0.5,
374
- );
375
- this.thermostatService.updateCharacteristic(
376
- this.hap.Characteristic.TargetHeatingCoolingState,
377
- this.hap.Characteristic.TargetHeatingCoolingState.AUTO,
378
- );
379
- mode = 'range';
380
- }
381
-
382
- this.set({ uuid: this.deviceData.nest_google_uuid, hvac_mode: mode });
383
-
384
- this?.log?.info?.('Set mode on "%s" to "%s"', this.deviceData.description, mode);
385
- }
386
- }
387
-
388
- getMode() {
389
- let currentMode = null;
390
-
391
- if (this.deviceData.hvac_mode.toUpperCase() === 'HEAT' || this.deviceData.hvac_mode.toUpperCase() === 'ECOHEAT') {
392
- // heating mode, either eco or normal;
393
- currentMode = this.hap.Characteristic.TargetHeatingCoolingState.HEAT;
394
- }
395
- if (this.deviceData.hvac_mode.toUpperCase() === 'COOL' || this.deviceData.hvac_mode.toUpperCase() === 'ECOCOOL') {
396
- // cooling mode, either eco or normal
397
- currentMode = this.hap.Characteristic.TargetHeatingCoolingState.COOL;
398
- }
399
- if (this.deviceData.hvac_mode.toUpperCase() === 'RANGE' || this.deviceData.hvac_mode.toUpperCase() === 'ECORANGE') {
400
- // range mode, either eco or normal
401
- currentMode = this.hap.Characteristic.TargetHeatingCoolingState.AUTO;
402
- }
403
- if (this.deviceData.hvac_mode.toUpperCase() === 'OFF' || (this.deviceData.can_cool === false && this.deviceData.can_heat === false)) {
404
- // off mode or no heating or cooling capability
405
- currentMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
406
- }
407
-
408
- return currentMode;
409
- }
410
-
411
- setTemperature(characteristic, temperature) {
412
- if (typeof characteristic === 'function' && typeof characteristic?.UUID === 'string') {
413
- if (
414
- characteristic.UUID === this.hap.Characteristic.TargetTemperature.UUID &&
415
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value !==
416
- this.hap.Characteristic.TargetHeatingCoolingState.AUTO
417
- ) {
418
- this.set({ uuid: this.deviceData.nest_google_uuid, target_temperature: temperature });
419
-
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
- );
430
- }
431
- if (
432
- characteristic.UUID === this.hap.Characteristic.HeatingThresholdTemperature.UUID &&
433
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value ===
434
- this.hap.Characteristic.TargetHeatingCoolingState.AUTO
435
- ) {
436
- this.set({ uuid: this.deviceData.nest_google_uuid, target_temperature_low: temperature });
437
-
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
- );
444
- }
445
- if (
446
- characteristic.UUID === this.hap.Characteristic.CoolingThresholdTemperature.UUID &&
447
- this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value ===
448
- this.hap.Characteristic.TargetHeatingCoolingState.AUTO
449
- ) {
450
- this.set({ uuid: this.deviceData.nest_google_uuid, target_temperature_high: temperature });
451
-
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
- );
458
- }
459
-
460
- this.thermostatService.updateCharacteristic(characteristic, temperature); // Update HomeKit with value
461
- }
462
- }
463
-
464
- getTemperature(characteristic) {
465
- let currentTemperature = null;
466
-
467
- if (typeof characteristic === 'function' && typeof characteristic?.UUID === 'string') {
468
- if (characteristic.UUID === this.hap.Characteristic.TargetTemperature.UUID) {
469
- currentTemperature = this.deviceData.target_temperature;
470
- }
471
- if (characteristic.UUID === this.hap.Characteristic.HeatingThresholdTemperature.UUID) {
472
- currentTemperature = this.deviceData.target_temperature_low;
473
- }
474
- if (characteristic.UUID === this.hap.Characteristic.CoolingThresholdTemperature.UUID) {
475
- currentTemperature = this.deviceData.target_temperature_high;
476
- }
477
- if (currentTemperature < MIN_TEMPERATURE) {
478
- currentTemperature = MIN_TEMPERATURE;
479
- }
480
- if (currentTemperature > MAX_TEMPERATURE) {
481
- currentTemperature = MAX_TEMPERATURE;
482
- }
483
- }
484
-
485
- return currentTemperature;
486
- }
487
-
488
- setChildlock(pin, value) {
489
- // TODO - pincode setting when turning on.
490
- // On REST API, writes to device.xxxxxxxx.temperature_lock_pin_hash. How is the hash calculated???
491
- // Do we set temperature range limits when child lock on??
492
-
493
- this.thermostatService.updateCharacteristic(this.hap.Characteristic.LockPhysicalControls, value); // Update HomeKit with value
494
- if (value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED) {
495
- // Set pin hash????
496
- }
497
- if (value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED) {
498
- // Clear pin hash????
499
- }
500
- this.set({
501
- uuid: this.deviceData.nest_google_uuid,
502
- temperature_lock: value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? true : false,
503
- });
504
-
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
- );
510
- }
511
-
512
- updateDevice(deviceData) {
268
+ onUpdate(deviceData) {
513
269
  if (
514
270
  typeof deviceData !== 'object' ||
515
271
  this.thermostatService === undefined ||
@@ -545,7 +301,7 @@ export default class NestThermostat extends HomeKitDevice {
545
301
  : this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED,
546
302
  );
547
303
 
548
- // Update air filter sttaus if has been added
304
+ // Update air filter status if has been added
549
305
  if (this.thermostatService.testCharacteristic(this.hap.Characteristic.FilterChangeIndication) === true) {
550
306
  this.thermostatService.updateCharacteristic(
551
307
  this.hap.Characteristic.FilterChangeIndication,
@@ -629,36 +385,6 @@ export default class NestThermostat extends HomeKitDevice {
629
385
  );
630
386
  }
631
387
 
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
- );
660
- }
661
-
662
388
  if (deviceData.can_cool !== this.deviceData.can_cool || deviceData.can_heat !== this.deviceData.can_heat) {
663
389
  // Heating and/cooling setup has changed on thermostat
664
390
 
@@ -787,7 +513,7 @@ export default class NestThermostat extends HomeKitDevice {
787
513
  this.hap.Characteristic.CurrentHeatingCoolingState,
788
514
  this.hap.Characteristic.CurrentHeatingCoolingState.COOL,
789
515
  );
790
- historyEntry.status = 3; // cooling
516
+ historyEntry.status = 1; // cooling
791
517
  }
792
518
  if (deviceData.hvac_state.toUpperCase() === 'OFF') {
793
519
  if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && typeof this.externalCool?.off === 'function') {
@@ -825,7 +551,12 @@ export default class NestThermostat extends HomeKitDevice {
825
551
  this.hap.Characteristic.Active,
826
552
  deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
827
553
  );
828
- historyEntry.status = 1; // fan
554
+
555
+ this.history(this.fanService, {
556
+ status: deviceData.fan_state === true ? 1 : 0,
557
+ temperature: deviceData.current_temperature,
558
+ humidity: deviceData.current_humidity,
559
+ });
829
560
  }
830
561
 
831
562
  if (this.dehumidifierService !== undefined) {
@@ -853,128 +584,336 @@ export default class NestThermostat extends HomeKitDevice {
853
584
  this.hap.Characteristic.Active,
854
585
  deviceData.dehumidifier_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
855
586
  );
856
- historyEntry.status = 4; // dehumidifier
857
- }
858
587
 
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);
588
+ this.history(this.dehumidifierService, {
589
+ status: deviceData.dehumidifier_state === true ? 1 : 0,
590
+ temperature: deviceData.current_temperature,
591
+ humidity: deviceData.current_humidity,
592
+ });
862
593
  }
863
594
 
864
595
  // Log thermostat metrics to history only if changed to previous recording
865
- if (this.thermostatService !== undefined && typeof this.historyService?.addHistory === 'function') {
866
- let tempEntry = this.historyService.lastHistory(this.thermostatService);
867
- if (
868
- tempEntry === undefined ||
869
- (typeof tempEntry === 'object' && tempEntry.status !== historyEntry.status) ||
870
- tempEntry.temperature !== deviceData.current_temperature ||
871
- JSON.stringify(tempEntry.target) !== JSON.stringify(historyEntry.target) ||
872
- tempEntry.humidity !== deviceData.current_humidity
873
- ) {
874
- this.historyService.addHistory(this.thermostatService, {
875
- time: Math.floor(Date.now() / 1000),
876
- status: historyEntry.status,
877
- temperature: deviceData.current_temperature,
878
- target: historyEntry.target,
879
- humidity: deviceData.current_humidity,
880
- });
881
- }
882
- }
596
+ this.history(this.thermostatService, {
597
+ status: historyEntry.status,
598
+ temperature: deviceData.current_temperature,
599
+ target: historyEntry.target,
600
+ humidity: deviceData.current_humidity,
601
+ });
883
602
 
884
- // Notify Eve App of device status changes if linked
885
- if (
886
- this.deviceData.eveHistory === true &&
887
- this.thermostatService !== undefined &&
888
- typeof this.historyService?.updateEveHome === 'function'
889
- ) {
890
- // Update our internal data with properties Eve will need to process
891
- this.deviceData.online = deviceData.online;
892
- this.deviceData.removed_from_base = deviceData.removed_from_base;
893
- this.deviceData.vacation_mode = deviceData.vacation_mode;
894
- this.deviceData.hvac_mode = deviceData.hvac_mode;
895
- this.deviceData.schedules = deviceData.schedules;
896
- this.deviceData.schedule_mode = deviceData.schedule_mode;
897
- this.historyService.updateEveHome(this.thermostatService, this.#EveHomeGetcommand.bind(this));
898
- }
603
+ // Update our internal data with properties Eve will need to process then Notify Eve App of device status changes if linked
604
+ this.deviceData.online = deviceData.online;
605
+ this.deviceData.removed_from_base = deviceData.removed_from_base;
606
+ this.deviceData.vacation_mode = deviceData.vacation_mode;
607
+ this.deviceData.hvac_mode = deviceData.hvac_mode;
608
+ this.deviceData.schedules = deviceData.schedules;
609
+ this.deviceData.schedule_mode = deviceData.schedule_mode;
610
+ this.historyService?.updateEveHome?.(this.thermostatService);
899
611
  }
900
612
 
901
- #EveHomeGetcommand(EveHomeGetData) {
902
- // Pass back extra data for Eve Thermo onGet() to process command
903
- // Data will already be an object, our only job is to add/modify it
904
- if (typeof EveHomeGetData === 'object') {
905
- EveHomeGetData.enableschedule = this.deviceData.schedule_mode === 'heat'; // Schedules on/off
906
- EveHomeGetData.attached = this.deviceData.online === true && this.deviceData.removed_from_base === false;
907
- EveHomeGetData.vacation = this.deviceData.vacation_mode === true; // Vaction mode on/off
908
- EveHomeGetData.vacationtemp = this.deviceData.vacation_mode === true ? EveHomeGetData.vacationtemp : null;
909
- EveHomeGetData.programs = []; // No programs yet, we'll process this below
910
- if (this.deviceData.schedule_mode.toUpperCase() === 'HEAT' || this.deviceData.schedule_mode.toUpperCase() === 'RANGE') {
911
- const DAYSOFWEEK = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
912
-
913
- Object.entries(this.deviceData.schedules).forEach(([day, schedules]) => {
613
+ onMessage(type, message) {
614
+ if (typeof type !== 'string' || type === '' || message === null || typeof message !== 'object' || message?.constructor !== Object) {
615
+ return;
616
+ }
617
+
618
+ if (type === HomeKitDevice?.EVEHOME?.GET) {
619
+ // Extend Eve Thermo GET payload with device state
620
+ message.enableschedule = this.deviceData.schedule_mode === 'heat';
621
+ message.attached = this.deviceData.online === true && this.deviceData.removed_from_base === false;
622
+ message.vacation = this.deviceData.vacation_mode === true;
623
+ message.programs = [];
624
+
625
+ if (['HEAT', 'RANGE'].includes(this.deviceData.schedule_mode?.toUpperCase?.()) === true) {
626
+ Object.entries(this.deviceData.schedules || {}).forEach(([day, entries]) => {
914
627
  let tempSchedule = [];
915
628
  let tempTemperatures = [];
916
- Object.values(schedules)
917
- .reverse()
918
- .forEach((schedule) => {
919
- if (schedule.entry_type === 'setpoint' && (schedule.type === 'HEAT' || schedule.type === 'RANGE')) {
920
- tempSchedule.push({
921
- start: schedule.time,
922
- duration: 0,
923
- temperature: typeof schedule['temp-min'] === 'number' ? schedule['temp-min'] : schedule.temp,
924
- });
925
- tempTemperatures.push(typeof schedule['temp-min'] === 'number' ? schedule['temp-min'] : schedule.temp);
926
- }
927
- });
928
629
 
929
- // Sort the schedule array by start time
930
- tempSchedule = tempSchedule.sort((a, b) => {
931
- if (a.start < b.start) {
932
- return -1;
630
+ Object.values(entries || {}).forEach((schedule) => {
631
+ if (schedule.entry_type === 'setpoint' && ['HEAT', 'RANGE'].includes(schedule.type)) {
632
+ let temp = typeof schedule['temp-min'] === 'number' ? schedule['temp-min'] : schedule.temp;
633
+ tempSchedule.push({ start: schedule.time, duration: 0, temperature: temp });
634
+ tempTemperatures.push(temp);
933
635
  }
934
636
  });
935
637
 
638
+ tempSchedule.sort((a, b) => a.start - b.start);
639
+
936
640
  let ecoTemp = tempTemperatures.length === 0 ? 0 : Math.min(...tempTemperatures);
937
641
  let comfortTemp = tempTemperatures.length === 0 ? 0 : Math.max(...tempTemperatures);
938
- let program = {};
939
- program.id = parseInt(day) + 1;
940
- program.days = DAYSOFWEEK[day];
941
- program.schedule = [];
942
- let lastTime = 86400; // seconds in a day
943
- Object.values(tempSchedule)
944
- .reverse()
945
- .forEach((schedule) => {
946
- if (schedule.temperature === comfortTemp) {
947
- // We only want to add the schedule time if its using the 'max' temperature
948
- program.schedule.push({
949
- start: schedule.start,
950
- duration: lastTime - schedule.start,
951
- ecotemp: ecoTemp,
952
- comforttemp: comfortTemp,
953
- });
954
- }
955
- lastTime = schedule.start;
956
- });
957
- EveHomeGetData.programs.push(program);
642
+
643
+ let program = {
644
+ id: parseInt(day),
645
+ days: isNaN(day) === false && DAYS_OF_WEEK_SHORT?.[day] !== undefined ? DAYS_OF_WEEK_SHORT[day].toLowerCase() : 'mon',
646
+ schedule: [],
647
+ };
648
+
649
+ let lastTime = 86400;
650
+ [...tempSchedule].reverse().forEach((entry) => {
651
+ if (entry.temperature === comfortTemp) {
652
+ program.schedule.push({
653
+ start: entry.start,
654
+ duration: lastTime - entry.start,
655
+ ecotemp: ecoTemp,
656
+ comforttemp: comfortTemp,
657
+ });
658
+ }
659
+ lastTime = entry.start;
660
+ });
661
+
662
+ message.programs.push(program);
958
663
  });
959
664
  }
665
+
666
+ return message;
667
+ }
668
+
669
+ if (type === HomeKitDevice?.EVEHOME?.SET) {
670
+ if (typeof message?.vacation?.status === 'boolean') {
671
+ this.message(HomeKitDevice.SET, { uuid: this.deviceData.nest_google_uuid, vacation_mode: message.vacation.status });
672
+ }
673
+
674
+ if (typeof message?.programs === 'object') {
675
+ // Future: convert to Nest format and apply via .set()
676
+ // this.message(HomeKitDevice.SET, { uuid: ..., days: { ... } });
677
+ }
960
678
  }
961
- return EveHomeGetData;
962
679
  }
963
680
 
964
- #EveHomeSetcommand(EveHomeSetData) {
965
- if (typeof EveHomeSetData !== 'object') {
681
+ setFan(fanState, speed) {
682
+ let currentState = this.fanService.getCharacteristic(this.hap.Characteristic.Active).value;
683
+ let currentSpeed = this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).value;
684
+
685
+ if (fanState !== currentState || speed !== currentSpeed) {
686
+ let isActive = fanState === this.hap.Characteristic.Active.ACTIVE;
687
+ let scaledSpeed = Math.round((speed / 100) * this.deviceData.fan_max_speed);
688
+
689
+ this.message(HomeKitDevice.SET, {
690
+ uuid: this.deviceData.nest_google_uuid,
691
+ fan_state: isActive,
692
+ fan_timer_speed: scaledSpeed,
693
+ });
694
+
695
+ this.fanService.updateCharacteristic(this.hap.Characteristic.Active, fanState);
696
+ this.fanService.updateCharacteristic(this.hap.Characteristic.RotationSpeed, speed);
697
+
698
+ this?.log?.info?.(
699
+ 'Set fan on thermostat "%s" to "%s"',
700
+ this.deviceData.description,
701
+ isActive ? 'On with fan speed of ' + speed + '%' : 'Off',
702
+ );
703
+ }
704
+ }
705
+
706
+ setDehumidifier(dehumidiferState) {
707
+ let isActive = dehumidiferState === this.hap.Characteristic.Active.ACTIVE;
708
+
709
+ this.message(HomeKitDevice.SET, {
710
+ uuid: this.deviceData.nest_google_uuid,
711
+ dehumidifier_state: isActive,
712
+ });
713
+
714
+ this.dehumidifierService.updateCharacteristic(this.hap.Characteristic.Active, dehumidiferState);
715
+
716
+ this?.log?.info?.(
717
+ 'Set dehumidifer on thermostat "%s" to "%s"',
718
+ this.deviceData.description,
719
+ isActive ? 'On with target humidity level of ' + this.deviceData.target_humidity + '%' : 'Off',
720
+ );
721
+ }
722
+
723
+ setDisplayUnit(temperatureUnit) {
724
+ let unit = temperatureUnit === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? 'C' : 'F';
725
+
726
+ this.message(HomeKitDevice.SET, {
727
+ uuid: this.deviceData.nest_google_uuid,
728
+ temperature_scale: unit,
729
+ });
730
+
731
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, temperatureUnit);
732
+
733
+ this?.log?.info?.('Set temperature units on thermostat "%s" to "%s"', this.deviceData.description, unit === 'C' ? '°C' : '°F');
734
+ }
735
+
736
+ setMode(thermostatMode) {
737
+ if (thermostatMode !== this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value) {
738
+ // Work out based on the HomeKit requested mode, what can the thermostat really switch too
739
+ // We may over-ride the requested HomeKit mode
740
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.HEAT && this.deviceData.can_heat === false) {
741
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
742
+ }
743
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.COOL && this.deviceData.can_cool === false) {
744
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
745
+ }
746
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) {
747
+ // Workaround for 'Hey Siri, turn on my thermostat'
748
+ // Appears to automatically request mode as 'auto', but we need to see what Nest device supports
749
+ if (this.deviceData.can_cool === true && this.deviceData.can_heat === false) {
750
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.COOL;
751
+ }
752
+ if (this.deviceData.can_cool === false && this.deviceData.can_heat === true) {
753
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.HEAT;
754
+ }
755
+ if (this.deviceData.can_cool === false && this.deviceData.can_heat === false) {
756
+ thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
757
+ }
758
+ }
759
+
760
+ let mode = '';
761
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.OFF) {
762
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature);
763
+ this.thermostatService.updateCharacteristic(
764
+ this.hap.Characteristic.TargetHeatingCoolingState,
765
+ this.hap.Characteristic.TargetHeatingCoolingState.OFF,
766
+ );
767
+ mode = 'off';
768
+ }
769
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.COOL) {
770
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature_high);
771
+ this.thermostatService.updateCharacteristic(
772
+ this.hap.Characteristic.TargetHeatingCoolingState,
773
+ this.hap.Characteristic.TargetHeatingCoolingState.COOL,
774
+ );
775
+ mode = 'cool';
776
+ }
777
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) {
778
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature_low);
779
+ this.thermostatService.updateCharacteristic(
780
+ this.hap.Characteristic.TargetHeatingCoolingState,
781
+ this.hap.Characteristic.TargetHeatingCoolingState.HEAT,
782
+ );
783
+ mode = 'heat';
784
+ }
785
+ if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) {
786
+ this.thermostatService.updateCharacteristic(
787
+ this.hap.Characteristic.TargetTemperature,
788
+ (this.deviceData.target_temperature_low + this.deviceData.target_temperature_high) * 0.5,
789
+ );
790
+ this.thermostatService.updateCharacteristic(
791
+ this.hap.Characteristic.TargetHeatingCoolingState,
792
+ this.hap.Characteristic.TargetHeatingCoolingState.AUTO,
793
+ );
794
+ mode = 'range';
795
+ }
796
+
797
+ this.message(HomeKitDevice.SET, { uuid: this.deviceData.nest_google_uuid, hvac_mode: mode });
798
+
799
+ this?.log?.info?.('Set mode on "%s" to "%s"', this.deviceData.description, mode);
800
+ }
801
+ }
802
+
803
+ getMode() {
804
+ let currentMode = null;
805
+ let mode = this.deviceData.hvac_mode.toUpperCase();
806
+
807
+ if (mode === 'HEAT' || mode === 'ECOHEAT') {
808
+ // heating mode, either eco or normal
809
+ currentMode = this.hap.Characteristic.TargetHeatingCoolingState.HEAT;
810
+ }
811
+ if (mode === 'COOL' || mode === 'ECOCOOL') {
812
+ // cooling mode, either eco or normal
813
+ currentMode = this.hap.Characteristic.TargetHeatingCoolingState.COOL;
814
+ }
815
+ if (mode === 'RANGE' || mode === 'ECORANGE') {
816
+ // range mode, either eco or normal
817
+ currentMode = this.hap.Characteristic.TargetHeatingCoolingState.AUTO;
818
+ }
819
+ if (mode === 'OFF' || (this.deviceData.can_cool === false && this.deviceData.can_heat === false)) {
820
+ // off mode or no heating or cooling capability
821
+ currentMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF;
822
+ }
823
+
824
+ return currentMode;
825
+ }
826
+
827
+ setTemperature(characteristic, temperature) {
828
+ if (typeof characteristic !== 'function' || typeof characteristic?.UUID !== 'string') {
966
829
  return;
967
830
  }
968
831
 
969
- if (typeof EveHomeSetData?.vacation === 'boolean') {
970
- this.set({ uuid: this.deviceData.nest_google_uuid, vacation_mode: EveHomeSetData.vacation.status });
832
+ let mode = this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value;
833
+ let isEco = this.deviceData.hvac_mode?.toUpperCase?.().includes('ECO') === true;
834
+ let scale = this.deviceData.temperature_scale?.toUpperCase?.() === 'F' ? 'F' : 'C';
835
+ let tempDisplay = (scale === 'F' ? (temperature * 9) / 5 + 32 : temperature).toFixed(1);
836
+ let tempUnit = scale === 'F' ? '°F' : '°C';
837
+ let ecoPrefix = isEco ? 'eco mode ' : '';
838
+
839
+ let targetKey = undefined;
840
+ let modeLabel = '';
841
+
842
+ if (
843
+ characteristic.UUID === this.hap.Characteristic.TargetTemperature.UUID &&
844
+ mode !== this.hap.Characteristic.TargetHeatingCoolingState.AUTO
845
+ ) {
846
+ targetKey = 'target_temperature';
847
+ modeLabel = mode === this.hap.Characteristic.TargetHeatingCoolingState.HEAT ? 'heating' : 'cooling';
848
+ } else if (
849
+ characteristic.UUID === this.hap.Characteristic.HeatingThresholdTemperature.UUID &&
850
+ mode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO
851
+ ) {
852
+ targetKey = 'target_temperature_low';
853
+ modeLabel = 'heating';
854
+ } else if (
855
+ characteristic.UUID === this.hap.Characteristic.CoolingThresholdTemperature.UUID &&
856
+ mode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO
857
+ ) {
858
+ targetKey = 'target_temperature_high';
859
+ modeLabel = 'cooling';
971
860
  }
972
- if (typeof EveHomeSetData?.programs === 'object') {
973
- //EveHomeSetData.programs.forEach((day) => {
974
- // Convert into Nest thermostat schedule format and set. Need to work this out
975
- // this.set({ uuid: this.deviceData.nest_google_uuid, days: { 6: { temp: 17, time: 13400, touched_at: Date.now() } } });
976
- //});
861
+
862
+ if (targetKey !== undefined) {
863
+ this.message(HomeKitDevice.SET, { uuid: this.deviceData.nest_google_uuid, [targetKey]: temperature });
864
+ this?.log?.info?.(
865
+ 'Set %s%s temperature on "%s" to "%s %s"',
866
+ ecoPrefix,
867
+ modeLabel,
868
+ this.deviceData.description,
869
+ tempDisplay,
870
+ tempUnit,
871
+ );
977
872
  }
873
+
874
+ this.thermostatService.updateCharacteristic(characteristic, temperature);
875
+ }
876
+
877
+ getTemperature(characteristic) {
878
+ if (typeof characteristic !== 'function' || typeof characteristic?.UUID !== 'string') {
879
+ return null;
880
+ }
881
+
882
+ let currentTemperature = {
883
+ [this.hap.Characteristic.TargetTemperature.UUID]: this.deviceData.target_temperature,
884
+ [this.hap.Characteristic.HeatingThresholdTemperature.UUID]: this.deviceData.target_temperature_low,
885
+ [this.hap.Characteristic.CoolingThresholdTemperature.UUID]: this.deviceData.target_temperature_high,
886
+ }[characteristic.UUID];
887
+
888
+ if (isNaN(currentTemperature) === false) {
889
+ currentTemperature = Math.min(Math.max(currentTemperature, THERMOSTAT_MIN_TEMPERATURE), THERMOSTAT_MAX_TEMPERATURE);
890
+ }
891
+
892
+ return currentTemperature;
893
+ }
894
+
895
+ setChildlock(pin, value) {
896
+ // TODO - pincode setting when turning on.
897
+ // On REST API, writes to device.xxxxxxxx.temperature_lock_pin_hash. How is the hash calculated???
898
+ // Do we set temperature range limits when child lock on??
899
+
900
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.LockPhysicalControls, value); // Update HomeKit with value
901
+ if (value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED) {
902
+ // Set pin hash????
903
+ }
904
+ if (value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED) {
905
+ // Clear pin hash????
906
+ }
907
+ this.message(HomeKitDevice.SET, {
908
+ uuid: this.deviceData.nest_google_uuid,
909
+ temperature_lock: value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? true : false,
910
+ });
911
+
912
+ this?.log?.info?.(
913
+ 'Setting Childlock on "%s" to "%s"',
914
+ this.deviceData.description,
915
+ value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? 'Enabled' : 'Disabled',
916
+ );
978
917
  }
979
918
 
980
919
  #setupFan() {
@@ -1020,18 +959,6 @@ export default class NestThermostat extends HomeKitDevice {
1020
959
  });
1021
960
  }
1022
961
 
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
962
  async #loadExternalModule(module, expectedFunctions = []) {
1036
963
  if (typeof module !== 'string' || module === '' || Array.isArray(expectedFunctions) === false) {
1037
964
  return undefined;
@@ -1084,9 +1011,9 @@ export default class NestThermostat extends HomeKitDevice {
1084
1011
  let shortName =
1085
1012
  typeof module === 'string'
1086
1013
  ? module
1087
- .trim()
1088
- .match(/'[^']*'|[^\s]+/)?.[0]
1089
- ?.replace(/^'(.*)'$/, '$1')
1014
+ .trim()
1015
+ .match(/'[^']*'|[^\s]+/)?.[0]
1016
+ ?.replace(/^'(.*)'$/, '$1')
1090
1017
  : '';
1091
1018
  this?.log?.warn?.('Failed to load external module "%s" for thermostat "%s"', shortName, this.deviceData.description);
1092
1019
  }
@@ -1095,8 +1022,517 @@ export default class NestThermostat extends HomeKitDevice {
1095
1022
  }
1096
1023
  }
1097
1024
 
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' : ''}`;
1025
+ // Function to process our RAW Nest or Google for this device type
1026
+ export function processRawData(log, rawData, config, deviceType = undefined) {
1027
+ if (
1028
+ rawData === null ||
1029
+ typeof rawData !== 'object' ||
1030
+ rawData?.constructor !== Object ||
1031
+ typeof config !== 'object' ||
1032
+ config?.constructor !== Object
1033
+ ) {
1034
+ return;
1035
+ }
1036
+
1037
+ const process_thermostat_data = (object_key, data) => {
1038
+ let processed = {};
1039
+ try {
1040
+ // Fix up data we need to
1041
+
1042
+ // If we have hot water control, it should be a 'UK/EU' model, so add that after the 'gen' tag in the model name
1043
+ data.model = data.has_hot_water_control === true ? data.model.replace(/\bgen\)/, 'gen, EU)') : data.model;
1044
+
1045
+ data = processCommonData(object_key, data, config);
1046
+ data.target_temperature_high = adjustTemperature(data.target_temperature_high, 'C', 'C', true);
1047
+ data.target_temperature_low = adjustTemperature(data.target_temperature_low, 'C', 'C', true);
1048
+ data.target_temperature = adjustTemperature(data.target_temperature, 'C', 'C', true);
1049
+ data.backplate_temperature = adjustTemperature(data.backplate_temperature, 'C', 'C', true);
1050
+ data.current_temperature = adjustTemperature(data.current_temperature, 'C', 'C', true);
1051
+ data.battery_level = scaleValue(data.battery_level, 3.6, 3.9, 0, 100);
1052
+
1053
+ processed = data;
1054
+ // eslint-disable-next-line no-unused-vars
1055
+ } catch (error) {
1056
+ // Empty
1057
+ }
1058
+ return processed;
1059
+ };
1060
+
1061
+ // Process data for any thermostat(s) we have in the raw data
1062
+ let devices = {};
1063
+ Object.entries(rawData)
1064
+ .filter(
1065
+ ([key, value]) =>
1066
+ key.startsWith('device.') === true ||
1067
+ (key.startsWith('DEVICE_') === true && PROTOBUF_RESOURCES.THERMOSTAT.includes(value.value?.device_info?.typeName) === true),
1068
+ )
1069
+ .forEach(([object_key, value]) => {
1070
+ let tempDevice = {};
1071
+ try {
1072
+ if (
1073
+ value?.source === DATA_SOURCE.GOOGLE &&
1074
+ value.value?.configuration_done?.deviceReady === true &&
1075
+ rawData?.[value.value?.device_info?.pairerId?.resourceId] !== undefined
1076
+ ) {
1077
+ let RESTTypeData = {};
1078
+ RESTTypeData.type = DEVICE_TYPE.THERMOSTAT;
1079
+ RESTTypeData.model = 'Thermostat (unknown)';
1080
+ if (value.value.device_info.typeName === 'nest.resource.NestLearningThermostat1Resource') {
1081
+ RESTTypeData.model = 'Learning Thermostat (1st gen)';
1082
+ }
1083
+ if (
1084
+ value.value.device_info.typeName === 'nest.resource.NestLearningThermostat2Resource' ||
1085
+ value.value.device_info.typeName === 'nest.resource.NestAmber1DisplayResource'
1086
+ ) {
1087
+ RESTTypeData.model = 'Learning Thermostat (2nd gen)';
1088
+ }
1089
+ if (
1090
+ value.value.device_info.typeName === 'nest.resource.NestLearningThermostat3Resource' ||
1091
+ value.value.device_info.typeName === 'nest.resource.NestAmber2DisplayResource'
1092
+ ) {
1093
+ RESTTypeData.model = 'Learning Thermostat (3rd gen)';
1094
+ }
1095
+ if (value.value.device_info.typeName === 'google.resource.GoogleBismuth1Resource') {
1096
+ RESTTypeData.model = 'Learning Thermostat (4th gen)';
1097
+ }
1098
+ if (
1099
+ value.value.device_info.typeName === 'nest.resource.NestOnyxResource' ||
1100
+ value.value.device_info.typeName === 'nest.resource.NestAgateDisplayResource'
1101
+ ) {
1102
+ RESTTypeData.model = 'Thermostat E (1st gen)';
1103
+ }
1104
+ if (value.value.device_info.typeName === 'google.resource.GoogleZirconium1Resource') {
1105
+ RESTTypeData.model = 'Thermostat (2020)';
1106
+ }
1107
+ RESTTypeData.softwareVersion = value.value.device_identity.softwareVersion;
1108
+ RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
1109
+ RESTTypeData.description = String(value.value?.label?.label ?? '');
1110
+ RESTTypeData.location = String(
1111
+ [
1112
+ ...Object.values(
1113
+ rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.predefinedWheres || {},
1114
+ ),
1115
+ ...Object.values(rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.customWheres || {}),
1116
+ ].find((where) => where?.whereId?.resourceId === value.value?.device_located_settings?.whereAnnotationRid?.resourceId)?.label
1117
+ ?.literal ?? '',
1118
+ );
1119
+ RESTTypeData.current_humidity =
1120
+ isNaN(value.value?.current_humidity?.humidityValue?.humidity?.value) === false
1121
+ ? Number(value.value.current_humidity.humidityValue.humidity.value)
1122
+ : 0.0;
1123
+ RESTTypeData.temperature_scale = value.value?.display_settings?.temperatureScale === 'TEMPERATURE_SCALE_F' ? 'F' : 'C';
1124
+ RESTTypeData.removed_from_base =
1125
+ Array.isArray(value.value?.display?.thermostatState) === true && value.value.display.thermostatState.includes('bpd') === true;
1126
+ RESTTypeData.backplate_temperature = parseFloat(value.value.backplate_temperature.temperatureValue.temperature.value);
1127
+ RESTTypeData.current_temperature = parseFloat(value.value.current_temperature.temperatureValue.temperature.value);
1128
+ RESTTypeData.battery_level = parseFloat(value.value.battery_voltage.batteryValue.batteryVoltage.value);
1129
+ RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
1130
+ RESTTypeData.leaf = value.value?.leaf?.active === true;
1131
+ RESTTypeData.can_cool =
1132
+ value.value?.hvac_equipment_capabilities?.hasStage1Cool === true ||
1133
+ value.value?.hvac_equipment_capabilities?.hasStage2Cool === true ||
1134
+ value.value?.hvac_equipment_capabilities?.hasStage3Cool === true;
1135
+ RESTTypeData.can_heat =
1136
+ value.value?.hvac_equipment_capabilities?.hasStage1Heat === true ||
1137
+ value.value?.hvac_equipment_capabilities?.hasStage2Heat === true ||
1138
+ value.value?.hvac_equipment_capabilities?.hasStage3Heat === true;
1139
+ RESTTypeData.temperature_lock = value.value?.temperature_lock_settings?.enabled === true;
1140
+ RESTTypeData.temperature_lock_pin_hash =
1141
+ value.value?.temperature_lock_settings?.enabled === true ? value.value.temperature_lock_settings.pinHash : '';
1142
+ RESTTypeData.away = value.value?.structure_mode?.structureMode === 'STRUCTURE_MODE_AWAY';
1143
+ RESTTypeData.occupancy = value.value?.structure_mode?.structureMode === 'STRUCTURE_MODE_HOME';
1144
+ //RESTTypeData.occupancy = (value.value.structure_mode.occupancy.activity === 'ACTIVITY_ACTIVE');
1145
+ RESTTypeData.vacation_mode = value.value?.structure_mode?.structureMode === 'STRUCTURE_MODE_VACATION';
1146
+
1147
+ // Work out current mode. ie: off, cool, heat, range and get temperature low/high and target
1148
+ RESTTypeData.hvac_mode =
1149
+ value.value?.target_temperature_settings?.enabled?.value === true &&
1150
+ value.value?.target_temperature_settings?.targetTemperature?.setpointType !== undefined
1151
+ ? value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase()
1152
+ : 'off';
1153
+ RESTTypeData.target_temperature_low =
1154
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value) === false
1155
+ ? Number(value.value.target_temperature_settings.targetTemperature.heatingTarget.value)
1156
+ : 0.0;
1157
+ RESTTypeData.target_temperature_high =
1158
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value) === false
1159
+ ? Number(value.value.target_temperature_settings.targetTemperature.coolingTarget.value)
1160
+ : 0.0;
1161
+ RESTTypeData.target_temperature =
1162
+ value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'SET_POINT_TYPE_COOL' &&
1163
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value) === false
1164
+ ? Number(value.value.target_temperature_settings.targetTemperature.coolingTarget.value)
1165
+ : value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'SET_POINT_TYPE_HEAT' &&
1166
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value) === false
1167
+ ? Number(value.value.target_temperature_settings.targetTemperature.heatingTarget.value)
1168
+ : value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'SET_POINT_TYPE_RANGE' &&
1169
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.coolingTarget?.value) === false &&
1170
+ isNaN(value.value?.target_temperature_settings?.targetTemperature?.heatingTarget?.value) === false
1171
+ ? (Number(value.value.target_temperature_settings.targetTemperature.coolingTarget.value) +
1172
+ Number(value.value.target_temperature_settings.targetTemperature.heatingTarget.value)) *
1173
+ 0.5
1174
+ : 0.0;
1175
+
1176
+ // Work out if eco mode is active and adjust temperature low/high and target
1177
+ if (value.value?.eco_mode_state?.ecoMode !== 'ECO_MODE_INACTIVE') {
1178
+ RESTTypeData.target_temperature_low = value.value.eco_mode_settings.ecoTemperatureHeat.value.value;
1179
+ RESTTypeData.target_temperature_high = value.value.eco_mode_settings.ecoTemperatureCool.value.value;
1180
+ if (
1181
+ value.value.eco_mode_settings.ecoTemperatureHeat.enabled === true &&
1182
+ value.value.eco_mode_settings.ecoTemperatureCool.enabled === false
1183
+ ) {
1184
+ RESTTypeData.target_temperature = value.value.eco_mode_settings.ecoTemperatureHeat.value.value;
1185
+ RESTTypeData.hvac_mode = 'ecoheat';
1186
+ }
1187
+ if (
1188
+ value.value.eco_mode_settings.ecoTemperatureHeat.enabled === false &&
1189
+ value.value.eco_mode_settings.ecoTemperatureCool.enabled === true
1190
+ ) {
1191
+ RESTTypeData.target_temperature = value.value.eco_mode_settings.ecoTemperatureCool.value.value;
1192
+ RESTTypeData.hvac_mode = 'ecocool';
1193
+ }
1194
+ if (
1195
+ value.value.eco_mode_settings.ecoTemperatureHeat.enabled === true &&
1196
+ value.value.eco_mode_settings.ecoTemperatureCool.enabled === true
1197
+ ) {
1198
+ RESTTypeData.target_temperature =
1199
+ (value.value.eco_mode_settings.ecoTemperatureCool.value.value +
1200
+ value.value.eco_mode_settings.ecoTemperatureHeat.value.value) *
1201
+ 0.5;
1202
+ RESTTypeData.hvac_mode = 'ecorange';
1203
+ }
1204
+ }
1205
+
1206
+ // Work out current state ie: heating, cooling etc
1207
+ RESTTypeData.hvac_state = 'off'; // By default, we're not heating or cooling
1208
+ if (
1209
+ value.value?.hvac_control?.hvacState?.coolStage1Active === true ||
1210
+ value.value?.hvac_control?.hvacState?.coolStage2Active === true ||
1211
+ value.value?.hvac_control?.hvacState?.coolStage2Active === true
1212
+ ) {
1213
+ // A cooling source is on, so we're in cooling mode
1214
+ RESTTypeData.hvac_state = 'cooling';
1215
+ }
1216
+ if (
1217
+ value.value?.hvac_control?.hvacState?.heatStage1Active === true ||
1218
+ value.value?.hvac_control?.hvacState?.heatStage2Active === true ||
1219
+ value.value?.hvac_control?.hvacState?.heatStage3Active === true ||
1220
+ value.value?.hvac_control?.hvacState?.alternateHeatStage1Active === true ||
1221
+ value.value?.hvac_control?.hvacState?.alternateHeatStage2Active === true ||
1222
+ value.value?.hvac_control?.hvacState?.auxiliaryHeatActive === true ||
1223
+ value.value?.hvac_control?.hvacState?.emergencyHeatActive === true
1224
+ ) {
1225
+ // A heating source is on, so we're in heating mode
1226
+ RESTTypeData.hvac_state = 'heating';
1227
+ }
1228
+
1229
+ // Fan details, on or off and max number of speeds supported
1230
+ RESTTypeData.has_fan =
1231
+ typeof value.value?.fan_control_capabilities?.maxAvailableSpeed === 'string' &&
1232
+ value.value.fan_control_capabilities.maxAvailableSpeed !== 'FAN_SPEED_SETTING_OFF';
1233
+ RESTTypeData.fan_state =
1234
+ isNaN(value.value?.fan_control_settings?.timerEnd?.seconds) === false &&
1235
+ Number(value.value.fan_control_settings.timerEnd.seconds) > 0;
1236
+ RESTTypeData.fan_timer_speed =
1237
+ value.value?.fan_control_settings?.timerSpeed?.includes?.('FAN_SPEED_SETTING_STAGE') === true &&
1238
+ isNaN(value.value.fan_control_settings.timerSpeed.split('FAN_SPEED_SETTING_STAGE')[1]) === false
1239
+ ? Number(value.value.fan_control_settings.timerSpeed.split('FAN_SPEED_SETTING_STAGE')[1])
1240
+ : 0;
1241
+ RESTTypeData.fan_max_speed =
1242
+ value.value?.fan_control_capabilities?.maxAvailableSpeed?.includes?.('FAN_SPEED_SETTING_STAGE') === true &&
1243
+ isNaN(value.value.fan_control_capabilities.maxAvailableSpeed.split('FAN_SPEED_SETTING_STAGE')[1]) === false
1244
+ ? Number(value.value.fan_control_capabilities.maxAvailableSpeed.split('FAN_SPEED_SETTING_STAGE')[1])
1245
+ : 0;
1246
+
1247
+ // Humidifier/dehumidifier details
1248
+ RESTTypeData.has_humidifier = value.value?.hvac_equipment_capabilities?.hasHumidifier === true;
1249
+ RESTTypeData.has_dehumidifier = value.value?.hvac_equipment_capabilities?.hasDehumidifier === true;
1250
+ RESTTypeData.target_humidity =
1251
+ isNaN(value.value?.humidity_control_settings?.targetHumidity?.value) === false
1252
+ ? Number(value.value.humidity_control_settings.targetHumidity.value)
1253
+ : 0.0;
1254
+ RESTTypeData.humidifier_state = value.value?.hvac_control?.hvacState?.humidifierActive === true;
1255
+ RESTTypeData.dehumidifier_state = value.value?.hvac_control?.hvacState?.dehumidifierActive === true;
1256
+
1257
+ // Air filter details
1258
+ RESTTypeData.has_air_filter = value.value?.hvac_equipment_capabilities?.hasAirFilter === true;
1259
+ RESTTypeData.filter_replacement_needed = value.value?.filter_reminder?.filterReplacementNeeded?.value === true;
1260
+
1261
+ // Process any temperature sensors associated with this thermostat
1262
+ RESTTypeData.active_rcs_sensor =
1263
+ value.value?.remote_comfort_sensing_settings?.activeRcsSelection?.activeRcsSensor !== undefined
1264
+ ? value.value.remote_comfort_sensing_settings.activeRcsSelection.activeRcsSensor.resourceId
1265
+ : '';
1266
+ RESTTypeData.linked_rcs_sensors = [];
1267
+ if (Array.isArray(value.value?.remote_comfort_sensing_settings?.associatedRcsSensors) === true) {
1268
+ value.value.remote_comfort_sensing_settings.associatedRcsSensors.forEach((sensor) => {
1269
+ if (typeof rawData?.[sensor?.deviceId?.resourceId]?.value === 'object') {
1270
+ rawData[sensor.deviceId.resourceId].value.associated_thermostat = object_key; // Sensor is linked to this thermostat
1271
+ }
1272
+
1273
+ RESTTypeData.linked_rcs_sensors.push(sensor.deviceId.resourceId);
1274
+ });
1275
+ }
1276
+
1277
+ RESTTypeData.schedule_mode =
1278
+ typeof value.value?.target_temperature_settings?.targetTemperature?.setpointType === 'string' &&
1279
+ value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase() !== 'off'
1280
+ ? value.value.target_temperature_settings.targetTemperature.setpointType.split('SET_POINT_TYPE_')[1].toLowerCase()
1281
+ : '';
1282
+ RESTTypeData.schedules = {};
1283
+ if (
1284
+ value.value[RESTTypeData.schedule_mode + '_schedule_settings']?.setpoints !== undefined &&
1285
+ value.value[RESTTypeData.schedule_mode + '_schedule_settings']?.type ===
1286
+ 'SET_POINT_SCHEDULE_TYPE_' + RESTTypeData.schedule_mode.toUpperCase()
1287
+ ) {
1288
+ Object.values(value.value[RESTTypeData.schedule_mode + '_schedule_settings'].setpoints).forEach((schedule) => {
1289
+ // Create Nest API schedule entries
1290
+ if (schedule?.dayOfWeek !== undefined) {
1291
+ let dayofWeekIndex = DAYS_OF_WEEK_FULL.indexOf(schedule.dayOfWeek.split('DAY_OF_WEEK_')[1]);
1292
+
1293
+ if (RESTTypeData.schedules?.[dayofWeekIndex] === undefined) {
1294
+ RESTTypeData.schedules[dayofWeekIndex] = {};
1295
+ }
1296
+
1297
+ RESTTypeData.schedules[dayofWeekIndex][Object.entries(RESTTypeData.schedules[dayofWeekIndex]).length] = {
1298
+ 'temp-min': adjustTemperature(schedule.heatingTarget.value, 'C', 'C', true),
1299
+ 'temp-max': adjustTemperature(schedule.coolingTarget.value, 'C', 'C', true),
1300
+ time: isNaN(schedule?.secondsInDay) === false ? Number(schedule.secondsInDay) : 0,
1301
+ type: RESTTypeData.schedule_mode.toUpperCase(),
1302
+ entry_type: 'setpoint',
1303
+ };
1304
+ }
1305
+ });
1306
+ }
1307
+
1308
+ tempDevice = process_thermostat_data(object_key, RESTTypeData);
1309
+ }
1310
+
1311
+ if (
1312
+ value?.source === DATA_SOURCE.NEST &&
1313
+ rawData?.['track.' + value.value?.serial_number] !== undefined &&
1314
+ rawData?.['link.' + value.value?.serial_number] !== undefined &&
1315
+ rawData?.['shared.' + value.value?.serial_number] !== undefined &&
1316
+ rawData?.['where.' + rawData?.['link.' + value.value?.serial_number]?.value?.structure?.split?.('.')[1]] !== undefined
1317
+ ) {
1318
+ let RESTTypeData = {};
1319
+ RESTTypeData.type = DEVICE_TYPE.THERMOSTAT;
1320
+ RESTTypeData.model = 'Thermostat (unknown)';
1321
+ if (value.value.serial_number.substring(0, 2) === '15') {
1322
+ RESTTypeData.model = 'Thermostat E (1st gen)'; // Nest Thermostat E
1323
+ }
1324
+ if (value.value.serial_number.substring(0, 2) === '09' || value.value.serial_number.substring(0, 2) === '10') {
1325
+ RESTTypeData.model = 'Learning Thermostat (3rd gen)'; // Nest Thermostat 3rd gen
1326
+ }
1327
+ if (value.value.serial_number.substring(0, 2) === '02') {
1328
+ RESTTypeData.model = 'Learning Thermostat (2nd gen)'; // Nest Thermostat 2nd gen
1329
+ }
1330
+ if (value.value.serial_number.substring(0, 2) === '01') {
1331
+ RESTTypeData.model = 'Learning Thermostat (1st gen)'; // Nest Thermostat 1st gen
1332
+ }
1333
+ RESTTypeData.softwareVersion = value.value.current_version;
1334
+ RESTTypeData.serialNumber = value.value.serial_number;
1335
+ RESTTypeData.description = String(rawData?.['shared.' + value.value.serial_number]?.value?.name ?? '');
1336
+ RESTTypeData.location = String(
1337
+ rawData?.['where.' + rawData?.['link.' + value.value.serial_number]?.value?.structure?.split?.('.')[1]]?.value?.wheres?.find(
1338
+ (where) => where?.where_id === value.value.where_id,
1339
+ )?.name ?? '',
1340
+ );
1341
+ RESTTypeData.current_humidity = value.value.current_humidity;
1342
+ RESTTypeData.temperature_scale = value.value.temperature_scale.toUpperCase() === 'F' ? 'F' : 'C';
1343
+ RESTTypeData.removed_from_base = value.value.nlclient_state.toUpperCase() === 'BPD';
1344
+ RESTTypeData.backplate_temperature = value.value.backplate_temperature;
1345
+ RESTTypeData.current_temperature = value.value.backplate_temperature;
1346
+ RESTTypeData.battery_level = value.value.battery_level;
1347
+ RESTTypeData.online = rawData?.['track.' + value.value.serial_number]?.value?.online === true;
1348
+ RESTTypeData.leaf = value.value.leaf === true;
1349
+ RESTTypeData.has_humidifier = value.value.has_humidifier === true;
1350
+ RESTTypeData.has_dehumidifier = value.value.has_dehumidifier === true;
1351
+ RESTTypeData.has_fan = value.value.has_fan === true;
1352
+ RESTTypeData.can_cool = rawData?.['shared.' + value.value.serial_number]?.value?.can_cool === true;
1353
+ RESTTypeData.can_heat = rawData?.['shared.' + value.value.serial_number]?.value?.can_heat === true;
1354
+ RESTTypeData.temperature_lock = value.value.temperature_lock === true;
1355
+ RESTTypeData.temperature_lock_pin_hash = value.value.temperature_lock_pin_hash;
1356
+ RESTTypeData.away = rawData?.[rawData?.['link.' + value.value.serial_number]?.value?.structure]?.value?.away === true;
1357
+ RESTTypeData.occupancy = RESTTypeData.away === false; // Occupancy is opposite of away status ie: away is false, then occupied
1358
+ RESTTypeData.vacation_mode =
1359
+ rawData[rawData?.['link.' + value.value.serial_number]?.value?.structure]?.value?.vacation_mode === true;
1360
+
1361
+ // Work out current mode. ie: off, cool, heat, range and get temperature low (heat) and high (cool)
1362
+ RESTTypeData.hvac_mode =
1363
+ rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type !== undefined
1364
+ ? rawData?.['shared.' + value.value.serial_number].value.target_temperature_type
1365
+ : 'off';
1366
+ RESTTypeData.target_temperature =
1367
+ isNaN(rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature) === false
1368
+ ? Number(rawData['shared.' + value.value.serial_number].value.target_temperature)
1369
+ : 0.0;
1370
+ RESTTypeData.target_temperature_low =
1371
+ isNaN(rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_low) === false
1372
+ ? Number(rawData['shared.' + value.value.serial_number].value.target_temperature_low)
1373
+ : 0.0;
1374
+ RESTTypeData.target_temperature_high =
1375
+ isNaN(rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_high) === false
1376
+ ? Number(rawData['shared.' + value.value.serial_number].value.target_temperature_high)
1377
+ : 0.0;
1378
+ if (rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type.toUpperCase() === 'COOL') {
1379
+ // Target temperature is the cooling point
1380
+ RESTTypeData.target_temperature =
1381
+ isNaN(rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_high) === false
1382
+ ? Number(rawData['shared.' + value.value.serial_number].value.target_temperature_high)
1383
+ : 0.0;
1384
+ }
1385
+ if (rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type.toUpperCase() === 'HEAT') {
1386
+ // Target temperature is the heating point
1387
+ RESTTypeData.target_temperature =
1388
+ isNaN(rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_low) === false
1389
+ ? Number(rawData['shared.' + value.value.serial_number].value.target_temperature_low)
1390
+ : 0.0;
1391
+ }
1392
+ if (rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_type.toUpperCase() === 'RANGE') {
1393
+ // Target temperature is in between the heating and cooling point
1394
+ RESTTypeData.target_temperature =
1395
+ isNaN(rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_low) === false &&
1396
+ isNaN(rawData?.['shared.' + value.value.serial_number]?.value?.target_temperature_high) === false
1397
+ ? (Number(rawData['shared.' + value.value.serial_number].value.target_temperature_low) +
1398
+ Number(rawData['shared.' + value.value.serial_number].value.target_temperature_high)) *
1399
+ 0.5
1400
+ : 0.0;
1401
+ }
1402
+
1403
+ // Work out if eco mode is active and adjust temperature low/high and target
1404
+ if (value.value.eco.mode.toUpperCase() === 'AUTO-ECO' || value.value.eco.mode.toUpperCase() === 'MANUAL-ECO') {
1405
+ RESTTypeData.target_temperature_low = value.value.away_temperature_low;
1406
+ RESTTypeData.target_temperature_high = value.value.away_temperature_high;
1407
+ if (value.value.away_temperature_high_enabled === true && value.value.away_temperature_low_enabled === false) {
1408
+ RESTTypeData.target_temperature = value.value.away_temperature_low;
1409
+ RESTTypeData.hvac_mode = 'ecoheat';
1410
+ }
1411
+ if (value.value.away_temperature_high_enabled === true && value.value.away_temperature_low_enabled === false) {
1412
+ RESTTypeData.target_temperature = value.value.away_temperature_high;
1413
+ RESTTypeData.hvac_mode = 'ecocool';
1414
+ }
1415
+ if (value.value.away_temperature_high_enabled === true && value.value.away_temperature_low_enabled === true) {
1416
+ RESTTypeData.target_temperature = (value.value.away_temperature_low + value.value.away_temperature_high) * 0.5;
1417
+ RESTTypeData.hvac_mode = 'ecorange';
1418
+ }
1419
+ }
1420
+
1421
+ // Work out current state ie: heating, cooling etc
1422
+ RESTTypeData.hvac_state = 'off'; // By default, we're not heating or cooling
1423
+ if (
1424
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_heater_state === true ||
1425
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_heat_x2_state === true ||
1426
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_heat_x3_state === true ||
1427
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_aux_heater_state === true ||
1428
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_alt_heat_x2_state === true ||
1429
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_emer_heat_state === true ||
1430
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_alt_heat_state === true
1431
+ ) {
1432
+ // A heating source is on, so we're in heating mode
1433
+ RESTTypeData.hvac_state = 'heating';
1434
+ }
1435
+ if (
1436
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_ac_state === true ||
1437
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_cool_x2_state === true ||
1438
+ rawData?.['shared.' + value.value.serial_number]?.value?.hvac_cool_x3_state === true
1439
+ ) {
1440
+ // A cooling source is on, so we're in cooling mode
1441
+ RESTTypeData.hvac_state = 'cooling';
1442
+ }
1443
+
1444
+ // Update fan status, on or off
1445
+ RESTTypeData.fan_state = isNaN(value.value?.fan_timer_timeout) === false && Number(value.value.fan_timer_timeout) > 0;
1446
+ RESTTypeData.fan_timer_speed =
1447
+ value.value?.fan_timer_speed?.includes?.('stage') === true && isNaN(value.value.fan_timer_speed.split('stage')[1]) === false
1448
+ ? Number(value.value.fan_timer_speed.split('stage')[1])
1449
+ : 0;
1450
+ RESTTypeData.fan_max_speed =
1451
+ value.value?.fan_capabilities?.includes?.('stage') === true && isNaN(value.value.fan_capabilities.split('stage')[1]) === false
1452
+ ? Number(value.value.fan_capabilities.split('stage')[1])
1453
+ : 0;
1454
+
1455
+ // Humidifier/dehumidifier details
1456
+ RESTTypeData.target_humidity = isNaN(value.value?.target_humidity) === false ? Number(value.value.target_humidity) : 0.0;
1457
+ RESTTypeData.humidifier_state = value.value.humidifier_state === true;
1458
+ RESTTypeData.dehumidifier_state = value.value.dehumidifier_state === true;
1459
+
1460
+ // Air filter details
1461
+ RESTTypeData.has_air_filter = value.value.has_air_filter === true;
1462
+ RESTTypeData.filter_replacement_needed = value.value.filter_replacement_needed === true;
1463
+
1464
+ // Process any temperature sensors associated with this thermostat
1465
+ RESTTypeData.active_rcs_sensor = '';
1466
+ RESTTypeData.linked_rcs_sensors = [];
1467
+ if (rawData?.['rcs_settings.' + value.value.serial_number]?.value?.associated_rcs_sensors !== undefined) {
1468
+ rawData?.['rcs_settings.' + value.value.serial_number].value.associated_rcs_sensors.forEach((sensor) => {
1469
+ if (typeof rawData[sensor]?.value === 'object') {
1470
+ rawData[sensor].value.associated_thermostat = object_key; // Sensor is linked to this thermostat
1471
+
1472
+ // Is this sensor the active one? If so, get some details about it
1473
+ if (
1474
+ rawData?.['rcs_settings.' + value.value.serial_number]?.value?.active_rcs_sensors !== undefined &&
1475
+ rawData?.['rcs_settings.' + value.value.serial_number]?.value?.active_rcs_sensors.includes(sensor)
1476
+ ) {
1477
+ RESTTypeData.active_rcs_sensor = rawData[sensor].value.serial_number.toUpperCase();
1478
+ RESTTypeData.current_temperature = rawData[sensor].value.current_temperature;
1479
+ }
1480
+ RESTTypeData.linked_rcs_sensors.push(rawData[sensor].value.serial_number.toUpperCase());
1481
+ }
1482
+ });
1483
+ }
1484
+
1485
+ // Get associated schedules
1486
+ if (rawData?.['schedule.' + value.value.serial_number] !== undefined) {
1487
+ Object.values(rawData['schedule.' + value.value.serial_number].value.days).forEach((schedules) => {
1488
+ Object.values(schedules).forEach((schedule) => {
1489
+ // Fix up temperatures in the schedule
1490
+ if (isNaN(schedule['temp']) === false) {
1491
+ schedule.temp = adjustTemperature(Number(schedule.temp), 'C', 'C', true);
1492
+ }
1493
+ if (isNaN(schedule['temp-min']) === false) {
1494
+ schedule['temp-min'] = adjustTemperature(Number(schedule['temp-min']), 'C', 'C', true);
1495
+ }
1496
+ if (isNaN(schedule['temp-max']) === false) {
1497
+ schedule['temp-max'] = adjustTemperature(Number(schedule['temp-max']), 'C', 'C', true);
1498
+ }
1499
+ });
1500
+ });
1501
+ RESTTypeData.schedules = rawData['schedule.' + value.value.serial_number].value.days;
1502
+ RESTTypeData.schedule_mode = rawData['schedule.' + value.value.serial_number].value.schedule_mode;
1503
+ }
1504
+
1505
+ tempDevice = process_thermostat_data(object_key, RESTTypeData);
1506
+ }
1507
+ // eslint-disable-next-line no-unused-vars
1508
+ } catch (error) {
1509
+ log?.debug?.('Error processing thermostat data for "%s"', object_key);
1510
+ }
1511
+
1512
+ if (
1513
+ Object.entries(tempDevice).length !== 0 &&
1514
+ typeof devices[tempDevice.serialNumber] === 'undefined' &&
1515
+ (deviceType === undefined || (typeof deviceType === 'string' && deviceType !== '' && tempDevice.type === deviceType))
1516
+ ) {
1517
+ let deviceOptions = config?.devices?.find(
1518
+ (device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
1519
+ );
1520
+ // Insert any extra options we've read in from configuration file for this device
1521
+ tempDevice.eveHistory = config.options.eveHistory === true || deviceOptions?.eveHistory === true;
1522
+ tempDevice.humiditySensor = deviceOptions?.humiditySensor === true;
1523
+ tempDevice.externalCool =
1524
+ typeof deviceOptions?.externalCool === 'string' && deviceOptions.externalCool !== '' ? deviceOptions.externalCool : undefined; // Config option for external cooling source
1525
+ tempDevice.externalHeat =
1526
+ typeof deviceOptions?.externalHeat === 'string' && deviceOptions.externalHeat !== '' ? deviceOptions.externalHeat : undefined; // Config option for external heating source
1527
+ tempDevice.externalFan =
1528
+ typeof deviceOptions?.externalFan === 'string' && deviceOptions.externalFan !== '' ? deviceOptions.externalFan : undefined; // Config option for external fan source
1529
+ tempDevice.externalDehumidifier =
1530
+ typeof deviceOptions?.externalDehumidifier === 'string' && deviceOptions.externalDehumidifier !== ''
1531
+ ? deviceOptions.externalDehumidifier
1532
+ : undefined; // Config option for external dehumidifier source
1533
+ devices[tempDevice.serialNumber] = tempDevice; // Store processed device
1534
+ }
1535
+ });
1536
+
1537
+ return devices;
1102
1538
  }