homebridge-nest-accfactory 0.3.0 → 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.
@@ -8,24 +8,33 @@
8
8
  import { setTimeout, clearTimeout } from 'node:timers';
9
9
 
10
10
  // Define external module requirements
11
- import NestCamera from './camera.js';
11
+ import NestCamera, { processRawData } from './camera.js';
12
+ export { processRawData };
12
13
 
13
14
  export default class NestDoorbell extends NestCamera {
14
15
  static TYPE = 'Doorbell';
15
- static VERSION = '2025.06.11';
16
+ static VERSION = '2025.07.24'; // Code version
16
17
 
17
18
  doorbellTimer = undefined; // Cooldown timer for doorbell events
18
19
  switchService = undefined; // HomeKit switch for enabling/disabling chime
19
20
 
20
- constructor(accessory, api, log, eventEmitter, deviceData) {
21
- super(accessory, api, log, eventEmitter, deviceData);
22
- }
23
-
24
21
  // Class functions
25
- setupDevice() {
26
- // Call parent to setup the common camera things. Once we return, we can add in the specifics for our doorbell
27
- // We pass in the HAP Doorbell controller constructor function here also
28
- super.setupDevice(this.hap.DoorbellController);
22
+ onAdd() {
23
+ // Setup motion services. This needs to be done before we setup the HomeKit doorbell controller
24
+ if (this.motionServices === undefined) {
25
+ this.createCameraMotionServices();
26
+ }
27
+
28
+ // Setup HomeKit doorbell controller
29
+ // Need to cleanup the CameraOperatingMode service. This is to allow seamless configuration
30
+ // switching between enabling hksv or not
31
+ // Thanks to @bcullman (Brad Ullman) for catching this
32
+ this.accessory.removeService(this.accessory.getService(this.hap.Service.CameraOperatingMode));
33
+ if (this.controller === undefined) {
34
+ // Establish the "camera" controller here as a doorbell specific one
35
+ // when onAdd is called for the base camera class, this will cconfigure our camera controller established here
36
+ this.controller = new this.hap.DoorbellController(this.generateControllerOptions());
37
+ }
29
38
 
30
39
  if (this.deviceData?.has_indoor_chime === true && this.deviceData?.chimeSwitch === true) {
31
40
  // Add service to allow automation and enabling/disabling indoor chiming.
@@ -37,7 +46,7 @@ export default class NestDoorbell extends NestCamera {
37
46
  onSet: (value) => {
38
47
  if (value !== this.deviceData.indoor_chime_enabled) {
39
48
  // only change indoor chime status value if different than on-device
40
- this.set({ uuid: this.deviceData.nest_google_uuid, indoor_chime_enabled: value });
49
+ this.message(NestDoorbell.SET, { uuid: this.deviceData.nest_google_uuid, indoor_chime_enabled: value });
41
50
 
42
51
  this?.log?.info?.('Indoor chime on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
43
52
  }
@@ -61,26 +70,19 @@ export default class NestDoorbell extends NestCamera {
61
70
  this.switchService !== undefined && this.postSetupDetail('Chime switch');
62
71
  }
63
72
 
64
- removeDevice() {
65
- super.removeDevice();
66
-
73
+ onRemove() {
67
74
  clearTimeout(this.doorbellTimer);
68
75
  this.doorbellTimer = undefined;
69
76
 
70
- if (this.switchService !== undefined) {
71
- this.accessory.removeService(this.switchService);
72
- }
77
+ this.accessory.removeService(this.switchService);
73
78
  this.switchService = undefined;
74
79
  }
75
80
 
76
- updateDevice(deviceData) {
81
+ onUpdate(deviceData) {
77
82
  if (typeof deviceData !== 'object' || this.controller === undefined) {
78
83
  return;
79
84
  }
80
85
 
81
- // Get the camera class todo all its updates first, then we'll handle the doorbell specific stuff
82
- super.updateDevice(deviceData);
83
-
84
86
  if (this.switchService !== undefined) {
85
87
  // Update status of indoor chime enable/disable switch
86
88
  this.switchService.updateCharacteristic(this.hap.Characteristic.On, deviceData.indoor_chime_enabled);
@@ -105,17 +107,9 @@ export default class NestDoorbell extends NestCamera {
105
107
  this.controller.ringDoorbell();
106
108
  }
107
109
 
108
- if (this.controller?.doorbellService !== undefined && typeof this.historyService?.addHistory === 'function') {
109
- // Record a doorbell press and unpress event to our history
110
- this.historyService.addHistory(this.controller.doorbellService, {
111
- time: Math.floor(Date.now() / 1000),
112
- status: 1,
113
- });
114
- this.historyService.addHistory(this.controller.doorbellService, {
115
- time: Math.floor(Date.now() / 1000),
116
- status: 0,
117
- });
118
- }
110
+ // Record a doorbell press and unpress event to our history
111
+ this.history(this.controller.doorbellService, { status: 1 }, { timegap: 2, force: true });
112
+ this.history(this.controller.doorbellService, { status: 0 }, { timegap: 2, force: true });
119
113
  }
120
114
  });
121
115
  }
@@ -5,23 +5,17 @@
5
5
  'use strict';
6
6
 
7
7
  // Define external module requirements
8
- import NestCamera from './camera.js';
8
+ import NestCamera, { processRawData } from './camera.js';
9
+ export { processRawData };
9
10
 
10
11
  export default class NestFloodlight extends NestCamera {
11
12
  static TYPE = 'FloodlightCamera';
12
- static VERSION = '2025.06.11';
13
+ static VERSION = '2025.07.25'; // Code version
13
14
 
14
15
  lightService = undefined; // HomeKit light
15
16
 
16
- constructor(accessory, api, log, eventEmitter, deviceData) {
17
- super(accessory, api, log, eventEmitter, deviceData);
18
- }
19
-
20
17
  // Class functions
21
- setupDevice() {
22
- // Call parent to setup the common camera things. Once we return, we can add in the specifics for our floodlight
23
- super.setupDevice();
24
-
18
+ onAdd() {
25
19
  if (this.deviceData.has_light === true) {
26
20
  // Add service to for a light, including brightness control
27
21
  this.lightService = this.addHKService(this.hap.Service.Lightbulb, '', 1);
@@ -29,7 +23,7 @@ export default class NestFloodlight extends NestCamera {
29
23
  props: { minStep: 10 }, // Light only goes in 10% increments
30
24
  onSet: (value) => {
31
25
  if (value !== this.deviceData.light_brightness) {
32
- this.set({ uuid: this.deviceData.nest_google_uuid, light_brightness: value });
26
+ this.message(NestFloodlight.SET, { uuid: this.deviceData.nest_google_uuid, light_brightness: value });
33
27
 
34
28
  this?.log?.info?.('Floodlight brightness on "%s" was set to "%s %"', this.deviceData.description, value);
35
29
  }
@@ -42,7 +36,7 @@ export default class NestFloodlight extends NestCamera {
42
36
  this.addHKCharacteristic(this.lightService, this.hap.Characteristic.On, {
43
37
  onSet: (value) => {
44
38
  if (value !== this.deviceData.light_enabled) {
45
- this.set({ uuid: this.deviceData.nest_google_uuid, light_enabled: value });
39
+ this.message(NestFloodlight.SET, { uuid: this.deviceData.nest_google_uuid, light_enabled: value });
46
40
 
47
41
  this?.log?.info?.('Floodlight on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
48
42
  }
@@ -58,30 +52,23 @@ export default class NestFloodlight extends NestCamera {
58
52
  if (this.lightService !== undefined) {
59
53
  this.accessory.removeService(this.lightService);
60
54
  }
61
- this.lightService === undefined;
55
+ this.lightService = undefined;
62
56
  }
63
57
 
64
58
  // Extra setup details for output
65
- this.lightService !== undefined && this.postSetupDetails('Light support');
59
+ this.lightService !== undefined && this.postSetupDetail('Light support');
66
60
  }
67
61
 
68
- removeDevice() {
69
- super.removeDevice();
70
-
71
- if (this.lightService !== undefined) {
72
- this.accessory.removeService(this.lightService);
73
- }
62
+ onRemove() {
63
+ this.accessory.removeService(this.lightService);
74
64
  this.lightService = undefined;
75
65
  }
76
66
 
77
- updateDevice(deviceData) {
67
+ onUpdate(deviceData) {
78
68
  if (typeof deviceData !== 'object' || this.controller === undefined) {
79
69
  return;
80
70
  }
81
71
 
82
- // Get the camera class todo all its updates first, then we'll handle the doorbell specific stuff
83
- super.updateDevice(deviceData);
84
-
85
72
  if (this.lightService !== undefined) {
86
73
  // Update status of light, including brightness
87
74
  this.lightService.updateCharacteristic(this.hap.Characteristic.On, deviceData.light_enabled);
@@ -5,13 +5,419 @@
5
5
  'use strict';
6
6
 
7
7
  // Define our modules
8
- import NestTemperatureSensor from './tempsensor.js';
8
+ import HomeKitDevice from '../HomeKitDevice.js';
9
+ import { processCommonData, adjustTemperature, parseDurationToSeconds } from '../utils.js';
9
10
 
10
- export default class NestHeatlink extends NestTemperatureSensor {
11
+ // Define constants
12
+ import {
13
+ DATA_SOURCE,
14
+ DEVICE_TYPE,
15
+ HOTWATER_MAX_TEMPERATURE,
16
+ HOTWATER_MIN_TEMPERATURE,
17
+ PROTOBUF_RESOURCES,
18
+ HOTWATER_BOOST_TIMES,
19
+ } from '../consts.js';
20
+
21
+ export default class NestHeatlink extends HomeKitDevice {
11
22
  static TYPE = 'Heatlink';
12
- static VERSION = '2025.06.11';
23
+ static VERSION = '2025.08.08'; // Code version
24
+
25
+ thermostatService = undefined; // Hotwater temperature control
26
+ switchService = undefined; // Hotwater heating boost control
27
+
28
+ onAdd() {
29
+ // Patch to avoid characteristic errors when setting initial property ranges
30
+ this.hap.Characteristic.TargetTemperature.prototype.getDefaultValue = () => {
31
+ return this.deviceData.hotwaterMinTemp; // start at minimum heating threshold
32
+ };
33
+ this.hap.Characteristic.TargetHeatingCoolingState.prototype.getDefaultValue = () => {
34
+ return this.hap.Characteristic.TargetHeatingCoolingState.HEAT; // Only heating
35
+ };
36
+
37
+ // If the heatlink supports hotwater temperature control
38
+ // Setup the thermostat service if not already present on the accessory, and link it to the Eve app if configured to do so
39
+ if (this.deviceData?.has_hot_water_control === true && this.deviceData?.has_hot_water_temperature === true) {
40
+ this.#setupHotwaterTemperature();
41
+ }
42
+
43
+ if (this.deviceData?.has_hot_water_control === false || this.deviceData?.has_hot_water_temperature === false) {
44
+ // No longer have hotwater temperature control configured and service present, so removed it
45
+ this.thermostatService = this.accessory.getService(this.hap.Service.Thermostat);
46
+ if (this.thermostatService !== undefined) {
47
+ this.accessory.removeService(this.thermostatService);
48
+ }
49
+ this.thermostatService = undefined;
50
+ }
51
+
52
+ // Setup hotwater boost heating service if supported by the thermostat and not already present on the accessory
53
+ if (this.deviceData?.has_hot_water_control === true) {
54
+ this.#setupHotwaterBoost();
55
+ }
56
+ if (this.deviceData?.has_hot_water_control === false) {
57
+ // No longer have hotwater heating configured and service present, so removed it
58
+ this.switchService = this.accessory.getService(this.hap.Service.Switch);
59
+ if (this.switchService !== undefined) {
60
+ this.accessory.removeService(this.switchService);
61
+ }
62
+ this.switchService = undefined;
63
+ }
64
+
65
+ // Extra setup details for output
66
+ this.thermostatService !== undefined &&
67
+ this.postSetupDetail('Temperature control (' + this.deviceData.hotwaterMinTemp + '–' + this.deviceData.hotwaterMaxTemp + '°C)');
68
+ }
69
+
70
+ onRemove() {
71
+ this.accessory.removeService(this.thermostatService);
72
+ this.accessory.removeService(this.switchService);
73
+ this.thermostatService = undefined;
74
+ this.switchService = undefined;
75
+ }
76
+
77
+ onUpdate(deviceData) {
78
+ if (typeof deviceData !== 'object') {
79
+ return;
80
+ }
81
+
82
+ // TODO dynamic changes to hotwater setup ie: boost control and temperature control
13
83
 
14
- constructor(accessory, api, log, eventEmitter, deviceData) {
15
- super(accessory, api, log, eventEmitter, deviceData);
84
+ if (this.thermostatService !== undefined) {
85
+ // Update when we have hot water temperature control
86
+ this.thermostatService.updateCharacteristic(
87
+ this.hap.Characteristic.TemperatureDisplayUnits,
88
+ deviceData.temperature_scale.toUpperCase() === 'C'
89
+ ? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS
90
+ : this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT,
91
+ );
92
+
93
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, deviceData.current_water_temperature);
94
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.hot_water_temperature);
95
+ this.thermostatService.updateCharacteristic(
96
+ this.hap.Characteristic.CurrentHeatingCoolingState,
97
+ deviceData.hot_water_active === true
98
+ ? this.hap.Characteristic.CurrentHeatingCoolingState.HEAT
99
+ : this.hap.Characteristic.CurrentHeatingCoolingState.OFF,
100
+ );
101
+
102
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.StatusActive, deviceData.online === true);
103
+
104
+ // Log thermostat metrics to history only if changed to previous recording
105
+ this.history(this.thermostatService, {
106
+ status: deviceData.hot_water_active === true ? 2 : 0, // 2 - heating water, 0 - not heating water
107
+ temperature: deviceData.current_water_temperature,
108
+ target: deviceData.hot_water_temperature,
109
+ });
110
+
111
+ // Update our internal data with properties Eve will need to process then Notify Eve App of device status changes if linked
112
+ this.deviceData.online = deviceData.online;
113
+ this.historyService?.updateEveHome?.(this.thermostatService);
114
+ }
115
+
116
+ if (this.switchService !== undefined) {
117
+ // Hotwater boost status on or off
118
+ this.switchService.updateCharacteristic(this.hap.Characteristic.On, deviceData.hot_water_boost_active === true);
119
+ }
120
+ }
121
+
122
+ onMessage(type, message) {
123
+ if (typeof type !== 'string' || type === '' || message === null || typeof message !== 'object' || message?.constructor !== Object) {
124
+ return;
125
+ }
126
+
127
+ if (type === HomeKitDevice?.HISTORY?.GET) {
128
+ // Extend Eve Thermo GET payload with device state
129
+ message.attached = this.deviceData.online === true;
130
+ return message;
131
+ }
16
132
  }
133
+
134
+ #setupHotwaterTemperature() {
135
+ this.thermostatService = this.addHKService(this.hap.Service.Thermostat, '', 1, { messages: this.message.bind(this) });
136
+ this.thermostatService.setPrimaryService();
137
+
138
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.StatusActive);
139
+
140
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TemperatureDisplayUnits, {
141
+ onSet: (value) => {
142
+ let unit = value === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? 'C' : 'F';
143
+
144
+ this.message(HomeKitDevice.SET, {
145
+ uuid: this.deviceData.associated_thermostat,
146
+ temperature_scale: unit,
147
+ });
148
+
149
+ this.thermostatService.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, value);
150
+
151
+ this?.log?.info?.('Set temperature units on heatlink "%s" to "%s"', this.deviceData.description, unit === 'C' ? '°C' : '°F');
152
+ },
153
+ onGet: () => {
154
+ return this.deviceData.temperature_scale.toUpperCase() === 'C'
155
+ ? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS
156
+ : this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT;
157
+ },
158
+ });
159
+
160
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.CurrentTemperature, {
161
+ props: { minStep: 0.5 },
162
+ onGet: () => {
163
+ return this.deviceData.current_water_temperature;
164
+ },
165
+ });
166
+
167
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TargetTemperature, {
168
+ props: {
169
+ minStep: 0.5,
170
+ minValue: this.deviceData.hotwaterMinTemp,
171
+ maxValue: this.deviceData.hotwaterMaxTemp,
172
+ },
173
+ onSet: (value) => {
174
+ if (value !== this.deviceData.hot_water_temperature) {
175
+ this.message(HomeKitDevice.SET, { uuid: this.deviceData.associated_thermostat, hot_water_temperature: value });
176
+
177
+ this?.log?.info?.('Set hotwater boiler temperature on heatlink "%s" to "%s °C"', this.deviceData.description, value);
178
+ }
179
+ },
180
+ onGet: () => {
181
+ return this.deviceData.hot_water_temperature;
182
+ },
183
+ });
184
+
185
+ // We only support heating for this thermostat service
186
+ this.addHKCharacteristic(this.thermostatService, this.hap.Characteristic.TargetHeatingCoolingState, {
187
+ props: {
188
+ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.HEAT],
189
+ },
190
+ });
191
+ }
192
+
193
+ #setupHotwaterBoost() {
194
+ this.switchService = this.addHKService(this.hap.Service.Switch, '', 1);
195
+
196
+ this.addHKCharacteristic(this.switchService, this.hap.Characteristic.On, {
197
+ onSet: (value) => {
198
+ if (value !== this.deviceData.hot_water_boost_active) {
199
+ this.message(HomeKitDevice.SET, {
200
+ uuid: this.deviceData.associated_thermostat,
201
+ hot_water_boost_active: { state: value === true, time: this.deviceData.hotwaterBoostTime },
202
+ });
203
+
204
+ this.switchService.updateCharacteristic(this.hap.Characteristic.On, value);
205
+
206
+ this?.log?.info?.(
207
+ 'Set hotwater boost heating on heatlink "%s" to "%s"',
208
+ this.deviceData.description,
209
+ value === true
210
+ ? 'On for ' +
211
+ (this.deviceData.hotwaterBoostTime >= 3600
212
+ ? Math.floor(this.deviceData.hotwaterBoostTime / 3600) +
213
+ ' hr' +
214
+ (Math.floor(this.deviceData.hotwaterBoostTime / 3600) > 1 ? 's ' : ' ')
215
+ : '') +
216
+ Math.floor((this.deviceData.hotwaterBoostTime % 3600) / 60) +
217
+ ' min' +
218
+ (Math.floor((this.deviceData.hotwaterBoostTime % 3600) / 60) !== 1 ? 's' : '')
219
+ : 'Off',
220
+ );
221
+ }
222
+ },
223
+ onGet: () => {
224
+ return this.deviceData?.hot_water_boost_active === true;
225
+ },
226
+ });
227
+ }
228
+ }
229
+
230
+ // Function to process our RAW Nest or Google for this device type
231
+ export function processRawData(log, rawData, config, deviceType = undefined) {
232
+ if (
233
+ rawData === null ||
234
+ typeof rawData !== 'object' ||
235
+ rawData?.constructor !== Object ||
236
+ typeof config !== 'object' ||
237
+ config?.constructor !== Object
238
+ ) {
239
+ return;
240
+ }
241
+
242
+ // Process data for any heatlink devices we have in the raw data
243
+ // We do this using any thermostat data
244
+ let devices = {};
245
+ Object.entries(rawData)
246
+ .filter(
247
+ ([key, value]) =>
248
+ key.startsWith('device.') === true ||
249
+ (key.startsWith('DEVICE_') === true && PROTOBUF_RESOURCES.THERMOSTAT.includes(value.value?.device_info?.typeName) === true),
250
+ )
251
+ .forEach(([object_key, value]) => {
252
+ let tempDevice = {};
253
+ try {
254
+ if (
255
+ value?.source === DATA_SOURCE.GOOGLE &&
256
+ value.value?.configuration_done?.deviceReady === true &&
257
+ typeof rawData?.[value.value?.device_info?.pairerId?.resourceId] === 'object' &&
258
+ ['HEAT_LINK_CONNECTION_TYPE_ON_OFF', 'HEAT_LINK_CONNECTION_TYPE_OPENTHERM'].some(
259
+ (type) =>
260
+ value.value?.heat_link_settings?.heatConnectionType === type ||
261
+ value.value?.heat_link_settings?.hotWaterConnectionType === type,
262
+ ) === true &&
263
+ (value.value?.heat_link?.heatLinkModel?.value?.trim?.() ?? '') !== '' &&
264
+ (value.value?.heat_link?.heatLinkSerialNumber?.value?.trim?.() ?? '') !== '' &&
265
+ (value.value?.heat_link?.heatLinkSwVersion?.value?.trim?.() ?? '') !== ''
266
+ ) {
267
+ tempDevice = processCommonData(
268
+ object_key,
269
+ {
270
+ type: DEVICE_TYPE.HEATLINK,
271
+ model:
272
+ value.value.heat_link.heatLinkModel.value.startsWith('Amber-2') === true
273
+ ? 'Heatlink for Learning Thermostat (3rd gen, EU)'
274
+ : value.value.heat_link.heatLinkModel.value.startsWith('Amber-1') === true
275
+ ? 'Heatlink for Learning Thermostat (2nd gen, EU)'
276
+ : value.value.heat_link.heatLinkModel.value.includes('Agate') === true
277
+ ? 'Heatlink for Thermostat E (1st gen, EU)'
278
+ : 'Heatlink (unknown - ' + value.value.heat_link.heatLinkModel + ')',
279
+ serialNumber: value.value.heat_link.heatLinkSerialNumber.value,
280
+ softwareVersion: value.value.heat_link.heatLinkSwVersion.value,
281
+ associated_thermostat: object_key, // Thermostat linked to
282
+ temperature_scale: value.value?.display_settings?.temperatureScale === 'TEMPERATURE_SCALE_F' ? 'F' : 'C',
283
+ online: value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE', // Use thermostat online status
284
+ has_hot_water_control: value.value?.hvac_equipment_capabilities?.hasHotWaterControl === true,
285
+ hot_water_active: value.value?.hot_water_trait?.boilerActive === true,
286
+ hot_water_boost_active:
287
+ isNaN(value.value?.hot_water_settings?.boostTimerEnd?.seconds) === false &&
288
+ Number(value.value.hot_water_settings.boostTimerEnd.seconds) > 0,
289
+ has_hot_water_temperature: value.value?.hvac_equipment_capabilities?.hasHotWaterTemperature === true,
290
+ current_water_temperature:
291
+ isNaN(value.value?.hot_water_trait?.temperature?.value) === false
292
+ ? adjustTemperature(Number(value.value.hot_water_trait.temperature.value), 'C', 'C', true)
293
+ : 0.0,
294
+ hot_water_temperature:
295
+ isNaN(value.value?.hot_water_settings?.temperature?.value) === false
296
+ ? adjustTemperature(Number(value.value.hot_water_settings.temperature.value), 'C', 'C', true)
297
+ : 0.0,
298
+ description: String(value.value?.label?.label ?? ''),
299
+ location: String(
300
+ [
301
+ ...Object.values(
302
+ rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.predefinedWheres || {},
303
+ ),
304
+ ...Object.values(
305
+ rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.customWheres || {},
306
+ ),
307
+ ].find((where) => where?.whereId?.resourceId === value.value?.device_located_settings?.whereAnnotationRid?.resourceId)
308
+ ?.label?.literal ?? '',
309
+ ),
310
+ },
311
+ config,
312
+ );
313
+ }
314
+
315
+ if (
316
+ value?.source === DATA_SOURCE.NEST &&
317
+ typeof rawData?.['track.' + value.value?.serial_number] === 'object' &&
318
+ typeof rawData?.['link.' + value.value?.serial_number] === 'object' &&
319
+ typeof rawData?.['shared.' + value.value?.serial_number] === 'object' &&
320
+ typeof rawData?.['where.' + rawData?.['link.' + value.value?.serial_number]?.value?.structure?.split?.('.')[1]] === 'object' &&
321
+ ['onoff', 'opentherm'].some(
322
+ (type) => value?.value?.heat_link_heat_type === type || value?.value?.heat_link_hot_water_type === type,
323
+ ) === true &&
324
+ (value.value?.heat_link_model?.trim?.() ?? '') !== '' &&
325
+ (value.value?.heat_link_serial_number?.trim?.() ?? '') !== '' &&
326
+ (value.value?.heat_link_sw_version?.trim?.() ?? '') !== ''
327
+ ) {
328
+ tempDevice = processCommonData(
329
+ object_key,
330
+ {
331
+ type: DEVICE_TYPE.HEATLINK,
332
+ model:
333
+ value.value.heat_link_model.startsWith('Amber-2') === true
334
+ ? 'Heatlink for Learning Thermostat (3rd gen, EU)'
335
+ : value.value.heat_link_model.startsWith('Amber-1') === true
336
+ ? 'Heatlink for Learning Thermostat (2nd gen, EU)'
337
+ : value.value.heat_link_model.includes('Agate') === true
338
+ ? 'Heatlink for Thermostat E (1st gen, EU)'
339
+ : 'Heatlink (unknown - ' + value.value.heat_link_model + ')',
340
+ serialNumber: value.value.heat_link_serial_number,
341
+ softwareVersion: value.value.heat_link_sw_version,
342
+ associated_thermostat: object_key, // Thermostat linked to
343
+ temperature_scale: value.value.temperature_scale.toUpperCase() === 'F' ? 'F' : 'C',
344
+ online: rawData?.['track.' + value.value.serial_number]?.value?.online === true, // Use thermostat online status
345
+ has_hot_water_control: value.value.has_hot_water_control === true,
346
+ hot_water_active: value.value?.hot_water_active === true,
347
+ hot_water_boost_active:
348
+ isNaN(value.value?.hot_water_boost_time_to_end) === false && Number(value.value.hot_water_boost_time_to_end) > 0,
349
+ has_hot_water_temperature: value.value?.has_hot_water_temperature === true,
350
+ hot_water_temperature:
351
+ isNaN(value.value?.hot_water_temperature) === false
352
+ ? adjustTemperature(Number(value.value.hot_water_temperature), 'C', 'C', true)
353
+ : 0.0,
354
+ current_water_temperature:
355
+ isNaN(value.value?.current_water_temperature) === false
356
+ ? adjustTemperature(Number(value.value.current_water_temperature), 'C', 'C', true)
357
+ : 0.0,
358
+ description: String(rawData?.['shared.' + value.value.serial_number]?.value?.name ?? ''),
359
+ location: String(
360
+ rawData?.[
361
+ 'where.' + rawData?.['link.' + value.value.serial_number]?.value?.structure?.split?.('.')[1]
362
+ ]?.value?.wheres?.find((where) => where?.where_id === value.value.where_id)?.name ?? '',
363
+ ),
364
+ },
365
+ config,
366
+ );
367
+ }
368
+ // eslint-disable-next-line no-unused-vars
369
+ } catch (error) {
370
+ log?.debug?.('Error processing heatlink data for "%s"', object_key);
371
+ }
372
+
373
+ if (
374
+ Object.entries(tempDevice).length !== 0 &&
375
+ typeof devices[tempDevice.serialNumber] === 'undefined' &&
376
+ (deviceType === undefined || (typeof deviceType === 'string' && deviceType !== '' && tempDevice.type === deviceType))
377
+ ) {
378
+ let deviceOptions = config?.devices?.find(
379
+ (device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
380
+ );
381
+ // Insert any extra options we've read in from configuration file for this device
382
+ tempDevice.eveHistory = config.options.eveHistory === true || deviceOptions?.eveHistory === true;
383
+
384
+ // Process hotwater boost time.. we only allow values matching app
385
+ tempDevice.hotwaterBoostTime = parseDurationToSeconds(deviceOptions?.hotwaterBoostTime, {
386
+ defaultValue: 1800, // 30 mins
387
+ min: 1800, // 30 mins
388
+ max: 7200, // 2 hrs
389
+ });
390
+
391
+ tempDevice.hotwaterBoostTime = HOTWATER_BOOST_TIMES.reduce((a, b) =>
392
+ Math.abs(tempDevice.hotwaterBoostTime - a) < Math.abs(tempDevice.hotwaterBoostTime - b) ? a : b,
393
+ );
394
+
395
+ tempDevice.hotwaterMinTemp =
396
+ isNaN(deviceOptions?.hotwaterMinTemp) === false
397
+ ? adjustTemperature(deviceOptions.hotwaterMinTemp, 'C', 'C', true)
398
+ : typeof deviceOptions?.hotwaterMinTemp === 'string' && /^([0-9.]+)\s*([CF])$/i.test(deviceOptions.hotwaterMinTemp)
399
+ ? adjustTemperature(
400
+ parseFloat(deviceOptions.hotwaterMinTemp.match(/^([0-9.]+)\s*([CF])$/i)[1]),
401
+ deviceOptions.hotwaterMinTemp.match(/^([0-9.]+)\s*([CF])$/i)[2],
402
+ 'C',
403
+ true,
404
+ )
405
+ : HOTWATER_MIN_TEMPERATURE; // 30c minimum
406
+
407
+ tempDevice.hotwaterMaxTemp =
408
+ isNaN(deviceOptions?.hotwaterMaxTemp) === false
409
+ ? adjustTemperature(deviceOptions.hotwaterMaxTemp, 'C', 'C', true)
410
+ : typeof deviceOptions?.hotwaterMaxTemp === 'string' && /^([0-9.]+)\s*([CF])$/i.test(deviceOptions.hotwaterMaxTemp)
411
+ ? adjustTemperature(
412
+ parseFloat(deviceOptions.hotwaterMaxTemp.match(/^([0-9.]+)\s*([CF])$/i)[1]),
413
+ deviceOptions.hotwaterMaxTemp.match(/^([0-9.]+)\s*([CF])$/i)[2],
414
+ 'C',
415
+ true,
416
+ )
417
+ : HOTWATER_MAX_TEMPERATURE; // 70c maximum
418
+ devices[tempDevice.serialNumber] = tempDevice; // Store processed device
419
+ }
420
+ });
421
+
422
+ return devices;
17
423
  }