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.
@@ -0,0 +1,309 @@
1
+ // Nest x Yale Lock (initial class - protobuf support only)
2
+ // Part of homebridge-nest-accfactory
3
+ //
4
+ // Mark Hulskamp
5
+ 'use strict';
6
+
7
+ // Define our modules
8
+ import HomeKitDevice from '../HomeKitDevice.js';
9
+ import { processCommonData, scaleValue } from '../utils.js';
10
+
11
+ // Define constants
12
+ import { DATA_SOURCE, DEVICE_TYPE, PROTOBUF_RESOURCES, LOW_BATTERY_LEVEL } from '../consts.js';
13
+
14
+ export default class NestLock extends HomeKitDevice {
15
+ static TYPE = 'Lock';
16
+ static VERSION = '2025.08.04'; // Code version
17
+
18
+ // Define lock bolt states
19
+ static STATE = {
20
+ JAMMED: 'jammed',
21
+ LOCKING: 'locking',
22
+ UNLOCKING: 'unlocking',
23
+ LOCKED: 'locked',
24
+ UNLOCKED: 'unlocked',
25
+ UNKNOWN: 'unknown',
26
+ };
27
+
28
+ static LAST_ACTION = {
29
+ PHYSICAL: 'physical',
30
+ KEYPAD: 'keypad',
31
+ REMOTE: 'remote',
32
+ IMPLICIT: 'implicit',
33
+ VOICE: 'voice',
34
+ };
35
+
36
+ lockService = undefined;
37
+ batteryService = undefined;
38
+
39
+ onAdd() {
40
+ // Setup lock service if not already present on the accessory and link it to the Eve app if configured to do so
41
+ this.lockService = this.addHKService(this.hap.Service.LockMechanism, '', 1, {});
42
+ this.lockService.setPrimaryService();
43
+
44
+ // Setup set characteristics
45
+ this.addHKCharacteristic(this.lockService, this.hap.Characteristic.LockCurrentState, {
46
+ onGet: () => {
47
+ return this.#currentState(this.deviceData);
48
+ },
49
+ });
50
+
51
+ this.addHKCharacteristic(this.lockService, this.hap.Characteristic.LockTargetState, {
52
+ onSet: (value) => {
53
+ let locked = value === this.hap.Characteristic.LockTargetState.SECURED;
54
+
55
+ this.message(HomeKitDevice.SET, {
56
+ uuid: this.deviceData.nest_google_uuid,
57
+ bolt_lock: locked,
58
+ });
59
+
60
+ this.lockService.updateCharacteristic(this.hap.Characteristic.LockTargetState, value);
61
+
62
+ this?.log?.info?.('Setting lock on "%s" to "%s"', this.deviceData.description, locked ? 'Locked' : 'Unlocked');
63
+ },
64
+ onGet: () => {
65
+ return this.#targetState(this.deviceData);
66
+ },
67
+ });
68
+
69
+ this.addHKCharacteristic(this.lockService, this.hap.Characteristic.LockManagementAutoSecurityTimeout, {
70
+ props: {
71
+ minValue: 0,
72
+ maxValue: this.deviceData.max_auto_relock_duration,
73
+ },
74
+ onSet: (value) => {
75
+ value = Math.floor(value); // Make a round number
76
+
77
+ if (value !== this.deviceData.auto_relock_duration) {
78
+ this.message(HomeKitDevice.SET, {
79
+ uuid: this.deviceData.nest_google_uuid,
80
+ auto_relock_duration: value,
81
+ });
82
+
83
+ this?.log?.info?.(
84
+ 'Setting lock auto-relocking duration on "%s" to "%s"',
85
+ this.deviceData.description,
86
+ value !== 0 ? value + ' seconds' : 'Disabled',
87
+ );
88
+ }
89
+ },
90
+ onGet: () => {
91
+ return this.deviceData.auto_relock_duration;
92
+ },
93
+ });
94
+
95
+ this.addHKCharacteristic(this.lockService, this.hap.Characteristic.LockLastKnownAction, {
96
+ onGet: () => {
97
+ return this.#lastAction(this.deviceData);
98
+ },
99
+ });
100
+
101
+ this.addHKCharacteristic(this.lockService, this.hap.Characteristic.StatusTampered, {
102
+ onGet: () => {
103
+ return this.deviceData.tampered === true
104
+ ? this.hap.Characteristic.StatusTampered.TAMPERED
105
+ : this.hap.Characteristic.StatusTampered.NOT_TAMPERED;
106
+ },
107
+ });
108
+
109
+ // Setup battery service if not already present on the accessory
110
+ this.batteryService = this.addHKService(this.hap.Service.Battery, '', 1);
111
+ this.batteryService.setHiddenService(true);
112
+ }
113
+
114
+ onRemove() {
115
+ this.accessory.removeService(this.lockService);
116
+ this.accessory.removeService(this.batteryService);
117
+ this.lockService = undefined;
118
+ this.batteryService = undefined;
119
+ }
120
+
121
+ onUpdate(deviceData) {
122
+ if (typeof deviceData !== 'object') {
123
+ return;
124
+ }
125
+
126
+ // Update lock state
127
+ this.lockService.updateCharacteristic(this.hap.Characteristic.LockCurrentState, this.#currentState(deviceData));
128
+ this.lockService.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.#targetState(deviceData));
129
+ this.lockService.updateCharacteristic(this.hap.Characteristic.LockLastKnownAction, this.#lastAction(deviceData));
130
+ this.lockService.updateCharacteristic(this.hap.Characteristic.LockManagementAutoSecurityTimeout, deviceData.auto_relock_duration);
131
+
132
+ // If device isn't online report in HomeKit
133
+ this.lockService.updateCharacteristic(
134
+ this.hap.Characteristic.StatusFault,
135
+ deviceData.online === true ? this.hap.Characteristic.StatusFault.NO_FAULT : this.hap.Characteristic.StatusFault.GENERAL_FAULT,
136
+ );
137
+
138
+ // Update battery level and status
139
+ this.batteryService.updateCharacteristic(this.hap.Characteristic.BatteryLevel, deviceData.battery_level);
140
+ this.batteryService.updateCharacteristic(
141
+ this.hap.Characteristic.StatusLowBattery,
142
+ deviceData.battery_level > LOW_BATTERY_LEVEL
143
+ ? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
144
+ : this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW,
145
+ );
146
+ this.batteryService.updateCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE);
147
+
148
+ // Update tampered state
149
+ this.lockService.updateCharacteristic(
150
+ this.hap.Characteristic.StatusTampered,
151
+ deviceData.tampered !== true ? this.hap.Characteristic.StatusTampered.NOT_TAMPERED : this.hap.Characteristic.StatusTampered.TAMPERED,
152
+ );
153
+
154
+ // Log lock status to history only if changed to previous recording
155
+ this.history(this.lockService, {
156
+ status: deviceData.bolt_state === NestLock.STATE.LOCKED ? 0 : 1, // 0 = locked, 1 = unlocked
157
+ });
158
+ }
159
+
160
+ #currentState(deviceData) {
161
+ return deviceData.bolt_state === NestLock.STATE.JAMMED
162
+ ? this.hap.Characteristic.LockCurrentState.JAMMED
163
+ : deviceData.bolt_state === NestLock.STATE.LOCKED
164
+ ? this.hap.Characteristic.LockCurrentState.SECURED
165
+ : deviceData.bolt_state === NestLock.STATE.UNLOCKED
166
+ ? this.hap.Characteristic.LockCurrentState.UNSECURED
167
+ : this.hap.Characteristic.LockCurrentState.UNKNOWN;
168
+ }
169
+
170
+ #targetState(deviceData) {
171
+ return deviceData.bolt_state === NestLock.STATE.LOCKED || deviceData.bolt_state === NestLock.STATE.LOCKING
172
+ ? this.hap.Characteristic.LockTargetState.SECURED
173
+ : this.hap.Characteristic.LockTargetState.UNSECURED;
174
+ }
175
+
176
+ #lastAction(deviceData) {
177
+ return deviceData.bolt_actor === NestLock.LAST_ACTION.PHYSICAL
178
+ ? deviceData.bolt_state === NestLock.STATE.LOCKED || deviceData.bolt_state === NestLock.STATE.LOCKING
179
+ ? this.hap.Characteristic.LockLastKnownAction.SECURED_PHYSICALLY
180
+ : this.hap.Characteristic.LockLastKnownAction.UNSECURED_PHYSICALLY
181
+ : deviceData.bolt_actor === NestLock.LAST_ACTION.KEYPAD
182
+ ? deviceData.bolt_state === NestLock.STATE.LOCKED || deviceData.bolt_state === NestLock.STATE.LOCKING
183
+ ? this.hap.Characteristic.LockLastKnownAction.SECURED_BY_KEYPAD
184
+ : this.hap.Characteristic.LockLastKnownAction.UNSECURED_BY_KEYPAD
185
+ : deviceData.bolt_actor === NestLock.LAST_ACTION.REMOTE || deviceData.LAST_ACTION === NestLock.LAST_ACTION.VOICE
186
+ ? deviceData.bolt_state === NestLock.STATE.LOCKED || deviceData.bolt_state === NestLock.STATE.LOCKING
187
+ ? this.hap.Characteristic.LockLastKnownAction.SECURED_REMOTELY
188
+ : this.hap.Characteristic.LockLastKnownAction.UNSECURED_REMOTELY
189
+ : this.hap.Characteristic.LockLastKnownAction.UNSECURED; // Fallback
190
+ }
191
+ }
192
+
193
+ // Function to process our RAW Nest or Google for this device type
194
+ export function processRawData(log, rawData, config, deviceType = undefined) {
195
+ if (
196
+ rawData === null ||
197
+ typeof rawData !== 'object' ||
198
+ rawData?.constructor !== Object ||
199
+ typeof config !== 'object' ||
200
+ config?.constructor !== Object
201
+ ) {
202
+ return;
203
+ }
204
+
205
+ // Process data for any lock(s) we have in the raw data
206
+ let devices = {};
207
+ Object.entries(rawData)
208
+ .filter(
209
+ ([key, value]) => key.startsWith('DEVICE_') === true && PROTOBUF_RESOURCES.LOCK.includes(value.value?.device_info?.typeName) === true,
210
+ )
211
+ .forEach(([object_key, value]) => {
212
+ let tempDevice = {};
213
+ try {
214
+ if (
215
+ value?.source === DATA_SOURCE.GOOGLE &&
216
+ value.value?.configuration_done?.deviceReady === true &&
217
+ rawData?.[value.value?.device_info?.pairerId?.resourceId] !== undefined
218
+ ) {
219
+ tempDevice = processCommonData(
220
+ object_key,
221
+ {
222
+ type: DEVICE_TYPE.LOCK,
223
+ model: 'x Yale Lock',
224
+ softwareVersion: value.value.device_identity.softwareVersion,
225
+ serialNumber: value.value.device_identity.serialNumber,
226
+ description: String(value.value?.label?.label ?? ''),
227
+ location: String(
228
+ [
229
+ ...Object.values(
230
+ rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.predefinedWheres || {},
231
+ ),
232
+ ...Object.values(
233
+ rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.customWheres || {},
234
+ ),
235
+ ].find((where) => where?.whereId?.resourceId === value.value?.device_located_settings?.whereAnnotationRid?.resourceId)
236
+ ?.label?.literal ?? '',
237
+ ),
238
+ online: value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE',
239
+ tampered: value.value?.tamper?.tamperState === 'TAMPER_STATE_TAMPERED',
240
+ bolt_state:
241
+ value.value?.bolt_lock?.actuatorState.startsWith('BOLT_ACTUATOR_STATE_JAMMED') === true
242
+ ? NestLock.STATE.JAMMED
243
+ : value.value?.bolt_lock?.actuatorState === 'BOLT_ACTUATOR_STATE_LOCKING'
244
+ ? NestLock.STATE.LOCKING
245
+ : value.value?.bolt_lock?.actuatorState === 'BOLT_ACTUATOR_STATE_UNLOCKING'
246
+ ? NestLock.STATE.UNLOCKING
247
+ : value.value?.bolt_lock?.lockedState === 'BOLT_LOCKED_STATE_LOCKED'
248
+ ? NestLock.STATE.LOCKED
249
+ : value.value?.bolt_lock?.lockedState === 'BOLT_LOCKED_STATE_UNLOCKED'
250
+ ? NestLock.STATE.UNLOCKED
251
+ : NestLock.STATE.UNKNOWN,
252
+ bolt_actor:
253
+ value.value?.bolt_lock?.boltLockActor?.method === 'BOLT_LOCK_ACTOR_METHOD_PHYSICAL'
254
+ ? NestLock.LAST_ACTION.PHYSICAL
255
+ : value.value?.bolt_lock?.boltLockActor?.method === 'BOLT_LOCK_ACTOR_METHOD_KEYPAD_PIN'
256
+ ? NestLock.LAST_ACTION.KEYPAD
257
+ : [
258
+ 'BOLT_LOCK_ACTOR_METHOD_REMOTE_USER_EXPLICIT',
259
+ 'BOLT_LOCK_ACTOR_METHOD_REMOTE_USER_IMPLICIT',
260
+ 'BOLT_LOCK_ACTOR_METHOD_REMOTE_USER_OTHER',
261
+ 'BOLT_LOCK_ACTOR_METHOD_REMOTE_DELEGATE',
262
+ ].includes(value.value?.bolt_lock?.boltLockActor?.method) === true
263
+ ? NestLock.LAST_ACTION.REMOTE
264
+ : value.value?.bolt_lock?.boltLockActor?.method === 'BOLT_LOCK_ACTOR_METHOD_VOICE_ASSISTANT'
265
+ ? NestLock.LAST_ACTION.VOICE
266
+ : ['BOLT_LOCK_ACTOR_METHOD_LOCAL_IMPLICIT', 'BOLT_LOCK_ACTOR_METHOD_LOW_POWER_SHUTDOWN'].includes(
267
+ value.value?.bolt_lock?.boltLockActor?.method,
268
+ ) === true
269
+ ? NestLock.LAST_ACTION.IMPLICIT
270
+ : NestLock.LAST_ACTION.PHYSICAL,
271
+ battery_level:
272
+ isNaN(value.value?.battery_power_source?.remaining?.remainingPercent) === false
273
+ ? scaleValue(Number(value.value?.battery_power_source?.remaining?.remainingPercent), 0, 1, 0, 100)
274
+ : 0,
275
+ auto_relock_duration:
276
+ isNaN(value.value?.bolt_lock_settings?.autoRelockDuration?.seconds) === false
277
+ ? Number(value.value.bolt_lock_settings.autoRelockDuration.seconds)
278
+ : 0,
279
+ max_auto_relock_duration:
280
+ isNaN(value.value?.bolt_lock_capabilities?.maxAutoRelockDuration?.seconds) === false
281
+ ? Number(value.value.bolt_lock_capabilities.maxAutoRelockDuration.seconds)
282
+ : 300,
283
+ },
284
+ config,
285
+ );
286
+ }
287
+ // eslint-disable-next-line no-unused-vars
288
+ } catch (error) {
289
+ log?.debug?.('Error processing lock data for "%s"', object_key);
290
+ }
291
+
292
+ if (
293
+ Object.entries(tempDevice).length !== 0 &&
294
+ typeof devices[tempDevice.serialNumber] === 'undefined' &&
295
+ (deviceType === undefined || (typeof deviceType === 'string' && deviceType !== '' && tempDevice.type === deviceType))
296
+ ) {
297
+ let deviceOptions = config?.devices?.find(
298
+ (device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
299
+ );
300
+
301
+ // Insert any extra options we've read in from configuration file for this device
302
+ tempDevice.eveHistory = config.options.eveHistory === true || deviceOptions?.eveHistory === true;
303
+
304
+ devices[tempDevice.serialNumber] = tempDevice; // Store processed device
305
+ }
306
+ });
307
+
308
+ return devices;
309
+ }
@@ -6,26 +6,33 @@
6
6
 
7
7
  // Define our modules
8
8
  import HomeKitDevice from '../HomeKitDevice.js';
9
+ import { processCommonData, scaleValue } from '../utils.js';
9
10
 
10
- const LOWBATTERYLEVEL = 10; // Low battery level percentage
11
+ // Define constants
12
+ import { LOW_BATTERY_LEVEL, DATA_SOURCE, PROTOBUF_RESOURCES, DEVICE_TYPE } from '../consts.js';
11
13
 
12
14
  export default class NestProtect extends HomeKitDevice {
13
15
  static TYPE = 'Protect';
14
- static VERSION = '2025.06.11';
16
+ static VERSION = '2025.08.04'; // Code version
15
17
 
16
18
  batteryService = undefined;
17
19
  smokeService = undefined;
18
20
  motionService = undefined;
19
21
  carbonMonoxideService = undefined;
20
22
 
21
- constructor(accessory, api, log, eventEmitter, deviceData) {
22
- super(accessory, api, log, eventEmitter, deviceData);
23
- }
24
-
25
23
  // Class functions
26
- setupDevice() {
24
+ onAdd() {
27
25
  // Setup the smoke sensor service if not already present on the accessory
28
- this.smokeService = this.addHKService(this.hap.Service.SmokeSensor, '', 1);
26
+ this.smokeService = this.addHKService(this.hap.Service.SmokeSensor, '', 1, {
27
+ messages: this.message.bind(this),
28
+ EveSmoke_lastalarmtest: this.deviceData.latest_alarm_test,
29
+ EveSmoke_alarmtest: this.deviceData.self_test_in_progress,
30
+ EveSmoke_heatstatus: this.deviceData.heat_status,
31
+ EveSmoke_hushedstate: this.deviceData.hushed_state,
32
+ Evesmoke_statusled: this.deviceData.ntp_green_led_enable,
33
+ EveSmoke_smoketestpassed: this.deviceData.smoke_test_passed,
34
+ EveSmoke_heattestpassed: this.deviceData.heat_test_passed,
35
+ });
29
36
  this.smokeService.setPrimaryService();
30
37
 
31
38
  this.addHKCharacteristic(this.smokeService, this.hap.Characteristic.StatusActive);
@@ -43,29 +50,20 @@ export default class NestProtect extends HomeKitDevice {
43
50
  this.motionService = this.addHKService(this.hap.Service.MotionSensor, '', 1);
44
51
  this.postSetupDetail('With motion sensor');
45
52
  }
53
+ }
46
54
 
47
- // Setup linkage to EveHome app if configured todo so
48
- if (
49
- this.deviceData?.eveHistory === true &&
50
- this.smokeService !== undefined &&
51
- typeof this.historyService?.linkToEveHome === 'function'
52
- ) {
53
- this.historyService.linkToEveHome(this.smokeService, {
54
- description: this.deviceData.description,
55
- getcommand: this.#EveHomeGetcommand.bind(this),
56
- setcommand: this.#EveHomeSetcommand.bind(this),
57
- EveSmoke_lastalarmtest: this.deviceData.latest_alarm_test,
58
- EveSmoke_alarmtest: this.deviceData.self_test_in_progress,
59
- EveSmoke_heatstatus: this.deviceData.heat_status,
60
- EveSmoke_hushedstate: this.deviceData.hushed_state,
61
- Evesmoke_statusled: this.deviceData.ntp_green_led_enable,
62
- EveSmoke_smoketestpassed: this.deviceData.smoke_test_passed,
63
- EveSmoke_heattestpassed: this.deviceData.heat_test_passed,
64
- });
65
- }
55
+ onRemove() {
56
+ this.accessory.removeService(this.smokeService);
57
+ this.accessory.removeService(this.carbonMonoxideService);
58
+ this.accessory.removeService(this.batteryService);
59
+ this.accessory.removeService(this.motionService);
60
+ this.smokeService = undefined;
61
+ this.carbonMonoxideService = undefined;
62
+ this.batteryService = undefined;
63
+ this.motionService = undefined;
66
64
  }
67
65
 
68
- updateDevice(deviceData) {
66
+ onUpdate(deviceData) {
69
67
  if (
70
68
  typeof deviceData !== 'object' ||
71
69
  this.smokeService === undefined ||
@@ -79,7 +77,7 @@ export default class NestProtect extends HomeKitDevice {
79
77
  this.batteryService.updateCharacteristic(this.hap.Characteristic.BatteryLevel, deviceData.battery_level);
80
78
  this.batteryService.updateCharacteristic(
81
79
  this.hap.Characteristic.StatusLowBattery,
82
- deviceData.battery_level > LOWBATTERYLEVEL && deviceData.battery_health_state === 0
80
+ deviceData.battery_level > LOW_BATTERY_LEVEL && deviceData.battery_health_state === 0
83
81
  ? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
84
82
  : this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW,
85
83
  );
@@ -148,56 +146,227 @@ export default class NestProtect extends HomeKitDevice {
148
146
  }
149
147
 
150
148
  // Log motion to history only if changed to previous recording
151
- if (deviceData.detected_motion !== this.deviceData.detected_motion && typeof this.historyService?.addHistory === 'function') {
152
- this.historyService.addHistory(this.motionService, {
153
- time: Math.floor(Date.now() / 1000),
154
- status: deviceData.detected_motion === true ? 1 : 0,
155
- });
156
- }
149
+ this.history(this.motionService, {
150
+ status: deviceData.detected_motion === true ? 1 : 0,
151
+ });
157
152
  }
158
153
 
159
- // Notify Eve App of device status changes if linked
160
- if (
161
- this.deviceData.eveHistory === true &&
162
- this.smokeService !== undefined &&
163
- typeof this.historyService?.updateEveHome === 'function'
164
- ) {
165
- // Update our internal data with properties Eve will need to process
166
- this.deviceData.latest_alarm_test = deviceData.latest_alarm_test;
167
- this.deviceData.self_test_in_progress = deviceData.self_test_in_progress;
168
- this.deviceData.heat_status = deviceData.heat_status;
169
- this.deviceData.ntp_green_led_enable = deviceData.ntp_green_led_enable;
170
- this.deviceData.smoke_test_passed = deviceData.smoke_test_passed;
171
- this.deviceData.heat_test_passed = deviceData.heat_test_passed;
172
- this.historyService.updateEveHome(this.smokeService, this.#EveHomeGetcommand.bind(this));
173
- }
154
+ // Update our internal data with properties Eve will need to process then Notify Eve App of device status changes if linked
155
+ this.deviceData.latest_alarm_test = deviceData.latest_alarm_test;
156
+ this.deviceData.self_test_in_progress = deviceData.self_test_in_progress;
157
+ this.deviceData.heat_status = deviceData.heat_status;
158
+ this.deviceData.ntp_green_led_enable = deviceData.ntp_green_led_enable;
159
+ this.deviceData.smoke_test_passed = deviceData.smoke_test_passed;
160
+ this.deviceData.heat_test_passed = deviceData.heat_test_passed;
161
+ this.historyService?.updateEveHome?.(this.smokeService);
174
162
  }
175
163
 
176
- #EveHomeGetcommand(EveHomeGetData) {
177
- // Pass back extra data for Eve Smoke onGet() to process command
178
- // Data will already be an object, our only job is to add/modify to it
179
- if (typeof EveHomeGetData === 'object') {
180
- EveHomeGetData.lastalarmtest = this.deviceData.latest_alarm_test;
181
- EveHomeGetData.alarmtest = this.deviceData.self_test_in_progress;
182
- EveHomeGetData.heatstatus = this.deviceData.heat_status;
183
- EveHomeGetData.statusled = this.deviceData.ntp_green_led_enable;
184
- EveHomeGetData.smoketestpassed = this.deviceData.smoke_test_passed;
185
- EveHomeGetData.heattestpassed = this.deviceData.heat_test_passed;
186
- EveHomeGetData.hushedstate = this.deviceData.hushed_state;
187
- }
188
- return EveHomeGetData;
189
- }
190
-
191
- #EveHomeSetcommand(EveHomeSetData) {
192
- if (typeof EveHomeSetData !== 'object') {
164
+ onMessage(type, message) {
165
+ if (typeof type !== 'string' || type === '' || typeof message !== 'object' || message === '') {
193
166
  return;
194
167
  }
195
168
 
196
- if (typeof EveHomeSetData?.alarmtest === 'boolean') {
197
- //this?.log?.info?.('Eve Smoke Alarm test', (EveHomeSetData.alarmtest === true ? 'start' : 'stop'));
169
+ if (type === HomeKitDevice?.HISTORY?.GET) {
170
+ // Pass back extra data for Eve Smoke onGet() to process command
171
+ // Data will already be an object, our only job is to add/modify to it
172
+ message.lastalarmtest = this.deviceData.latest_alarm_test;
173
+ message.alarmtest = this.deviceData.self_test_in_progress;
174
+ message.heatstatus = this.deviceData.heat_status;
175
+ message.statusled = this.deviceData.ntp_green_led_enable;
176
+ message.smoketestpassed = this.deviceData.smoke_test_passed;
177
+ message.heattestpassed = this.deviceData.heat_test_passed;
178
+ message.hushedstate = this.deviceData.hushed_state;
179
+ return message;
198
180
  }
199
- if (typeof EveHomeSetData?.statusled === 'boolean') {
200
- this.set({ uuid: this.deviceData.nest_google_uuid, ntp_green_led_enable: EveHomeSetData.statusled });
181
+
182
+ if (type === HomeKitDevice?.HISTORY?.SET) {
183
+ if (typeof message?.alarmtest === 'boolean') {
184
+ // TODO - How do we trigger an alarm test :-)
185
+ //this?.log?.info?.('Eve Smoke Alarm test', (message.alarmtest === true ? 'start' : 'stop'));
186
+ }
187
+ if (typeof message?.statusled === 'boolean') {
188
+ this.message(HomeKitDevice.SET, { uuid: this.deviceData.nest_google_uuid, ntp_green_led_enable: message.statusled });
189
+ }
201
190
  }
202
191
  }
203
192
  }
193
+
194
+ // Function to process our RAW Nest or Google for this device type
195
+ export function processRawData(log, rawData, config, deviceType = undefined) {
196
+ if (
197
+ rawData === null ||
198
+ typeof rawData !== 'object' ||
199
+ rawData?.constructor !== Object ||
200
+ typeof config !== 'object' ||
201
+ config?.constructor !== Object
202
+ ) {
203
+ return;
204
+ }
205
+
206
+ // Process data for any smoke detectors we have in the raw data
207
+ let devices = {};
208
+ Object.entries(rawData)
209
+ .filter(
210
+ ([key, value]) =>
211
+ key.startsWith('topaz.') === true ||
212
+ (key.startsWith('DEVICE_') === true && PROTOBUF_RESOURCES.PROTECT.includes(value.value?.device_info?.typeName) === true),
213
+ )
214
+ .forEach(([object_key, value]) => {
215
+ let tempDevice = {};
216
+ try {
217
+ if (
218
+ value?.source === DATA_SOURCE.GOOGLE &&
219
+ value.value?.configuration_done?.deviceReady === true &&
220
+ rawData?.[value.value?.device_info?.pairerId?.resourceId] !== undefined
221
+ ) {
222
+ tempDevice = processCommonData(
223
+ object_key,
224
+ {
225
+ type: DEVICE_TYPE.PROTECT,
226
+ model:
227
+ value.value.device_info.typeName === 'nest.resource.NestProtect1LinePoweredResource'
228
+ ? 'Protect (1st gen, wired)'
229
+ : value.value.device_info.typeName === 'nest.resource.NestProtect1BatteryPoweredResource'
230
+ ? 'Protect (1st gen, battery)'
231
+ : value.value.device_info.typeName === 'nest.resource.NestProtect2LinePoweredResource'
232
+ ? 'Protect (2nd gen, wired)'
233
+ : value.value.device_info.typeName === 'nest.resource.NestProtect2BatteryPoweredResource'
234
+ ? 'Protect (2nd gen, battery)'
235
+ : 'Protect (unknown)',
236
+ softwareVersion: value.value.device_identity.softwareVersion,
237
+ serialNumber: value.value.device_identity.serialNumber,
238
+ description: String(value.value?.label?.label ?? ''),
239
+ location: String(
240
+ [
241
+ ...Object.values(
242
+ rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.predefinedWheres || {},
243
+ ),
244
+ ...Object.values(
245
+ rawData?.[value.value?.device_info?.pairerId?.resourceId]?.value?.located_annotations?.customWheres || {},
246
+ ),
247
+ ].find((where) => where?.whereId?.resourceId === value.value?.device_located_settings?.whereAnnotationRid?.resourceId)
248
+ ?.label?.literal ?? '',
249
+ ),
250
+ online: value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE',
251
+ line_power_present: value.value?.wall_power?.status === 'POWER_SOURCE_STATUS_ACTIVE',
252
+ wired_or_battery: typeof value.value?.wall_power?.status === 'string' ? 0 : 1,
253
+ battery_level:
254
+ isNaN(value.value?.battery_voltage_bank1?.batteryValue?.batteryVoltage?.value) === false
255
+ ? scaleValue(Number(value.value.battery_voltage_bank1.batteryValue.batteryVoltage.value), 0, 5.4, 0, 100)
256
+ : 0,
257
+ battery_health_state:
258
+ value.value?.battery_voltage_bank0?.faultInformation === undefined &&
259
+ value.value?.battery_voltage_bank1?.faultInformation === undefined
260
+ ? 0
261
+ : 1,
262
+ smoke_status: value.value?.safety_alarm_smoke?.alarmState === 'ALARM_STATE_ALARM',
263
+ co_status: value.value?.safety_alarm_co?.alarmState === 'ALARM_STATE_ALARM',
264
+ heat_status: false, // TODO <- need to find in protobuf
265
+ hushed_state:
266
+ value.value?.safety_alarm_smoke?.silenceState === 'SILENCE_STATE_SILENCED' ||
267
+ value.value?.safety_alarm_co?.silenceState === 'SILENCE_STATE_SILENCED',
268
+ ntp_green_led: value.value?.night_time_promise_settings?.greenLedEnabled === true,
269
+ smoke_test_passed:
270
+ typeof value.value.safety_summary?.warningDevices?.failures === 'object'
271
+ ? value.value.safety_summary.warningDevices.failures.includes('FAILURE_TYPE_SMOKE') === false
272
+ : true,
273
+ heat_test_passed:
274
+ typeof value.value.safety_summary?.warningDevices?.failures === 'object'
275
+ ? value.value.safety_summary.warningDevices.failures.includes('FAILURE_TYPE_TEMP') === false
276
+ : true,
277
+ latest_alarm_test:
278
+ isNaN(value.value?.self_test?.lastMstEnd?.seconds) === false ? Number(value.value.self_test.lastMstEnd.seconds) : 0,
279
+ self_test_in_progress:
280
+ value.value?.legacy_structure_self_test?.mstInProgress === true ||
281
+ value.value?.legacy_structure_self_test?.astInProgress === true,
282
+ replacement_date:
283
+ isNaN(value.value?.legacy_protect_device_settings?.replaceByDate?.seconds) === false
284
+ ? Number(value.value.legacy_protect_device_settings.replaceByDate.seconds)
285
+ : 0,
286
+ topaz_hush_key:
287
+ typeof value.value?.safety_structure_settings?.structureHushKey === 'string'
288
+ ? value.value.safety_structure_settings.structureHushKey
289
+ : '',
290
+ detected_motion:
291
+ value.value?.legacy_protect_device_info?.autoAway !== true || value.value?.structure_mode?.occupancy === 'ACTIVITY_ACTIVE',
292
+ },
293
+ config,
294
+ );
295
+ }
296
+
297
+ if (
298
+ value?.source === DATA_SOURCE.NEST &&
299
+ rawData?.['where.' + value.value?.structure_id] !== undefined &&
300
+ rawData?.['safety.' + value.value?.structure_id] !== undefined &&
301
+ rawData?.['widget_track.' + value.value?.thread_mac_address?.toUpperCase()] !== undefined &&
302
+ rawData?.['safety.' + value.value?.structure_id] !== undefined
303
+ ) {
304
+ tempDevice = processCommonData(
305
+ object_key,
306
+ {
307
+ type: DEVICE_TYPE.PROTECT,
308
+ model: (() => {
309
+ let model =
310
+ value.value.serial_number.substring(0, 2) === '06'
311
+ ? 'Protect (2nd gen)'
312
+ : value.value.serial_number.substring(0, 2) === '05'
313
+ ? 'Protect (1st gen)'
314
+ : 'Protect (unknown)';
315
+ return value.value.wired_or_battery === 1
316
+ ? model.replace(/\bgen\)/, 'gen, battery)')
317
+ : value.value.wired_or_battery === 0
318
+ ? model.replace(/\bgen\)/, 'gen, wired)')
319
+ : model;
320
+ })(),
321
+ softwareVersion: value.value.software_version,
322
+ serialNumber: value.value.serial_number,
323
+ description: String(value.value?.description ?? ''),
324
+ location: String(
325
+ rawData?.['where.' + value.value.structure_id]?.value?.wheres?.find((where) => where?.where_id === value.value.where_id)
326
+ ?.name ?? '',
327
+ ),
328
+ online: rawData?.['widget_track.' + value.value.thread_mac_address.toUpperCase()]?.value?.online === true,
329
+ line_power_present: value.value.line_power_present === true,
330
+ wired_or_battery: value.value.wired_or_battery,
331
+ battery_level: scaleValue(value.value.battery_level, 0, 5400, 0, 100),
332
+ battery_health_state: value.value.battery_health_state,
333
+ smoke_status: value.value.smoke_status !== 0,
334
+ co_status: value.value.co_status !== 0,
335
+ heat_status: value.value.heat_status !== 0,
336
+ hushed_state: value.value.hushed_state === true,
337
+ ntp_green_led_enable: value.value.ntp_green_led_enable === true,
338
+ smoke_test_passed: value.value.component_smoke_test_passed === true,
339
+ heat_test_passed: value.value.component_temp_test_passed === true,
340
+ latest_alarm_test: value.value.latest_manual_test_end_utc_secs,
341
+ self_test_in_progress: rawData?.['safety.' + value.value.structure_id]?.value?.manual_self_test_in_progress === true,
342
+ replacement_date: value.value.replace_by_date_utc_secs,
343
+ topaz_hush_key:
344
+ typeof rawData?.['structure.' + value.value.structure_id]?.value?.topaz_hush_key === 'string'
345
+ ? rawData['structure.' + value.value.structure_id].value.topaz_hush_key
346
+ : '',
347
+ detected_motion: value.value.auto_away === false,
348
+ },
349
+ config,
350
+ );
351
+ }
352
+ // eslint-disable-next-line no-unused-vars
353
+ } catch (error) {
354
+ log?.debug?.('Error processing protect data for "%s"', object_key);
355
+ }
356
+
357
+ if (
358
+ Object.entries(tempDevice).length !== 0 &&
359
+ typeof devices[tempDevice.serialNumber] === 'undefined' &&
360
+ (deviceType === undefined || (typeof deviceType === 'string' && deviceType !== '' && tempDevice.type === deviceType))
361
+ ) {
362
+ let deviceOptions = config?.devices?.find(
363
+ (device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
364
+ );
365
+ // Insert any extra options we've read in from configuration file for this device
366
+ tempDevice.eveHistory = config.options.eveHistory === true || deviceOptions?.eveHistory === true;
367
+ devices[tempDevice.serialNumber] = tempDevice; // Store processed device
368
+ }
369
+ });
370
+
371
+ return devices;
372
+ }