homebridge-nest-accfactory 0.2.11 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +14 -7
  3. package/config.schema.json +118 -0
  4. package/dist/HomeKitDevice.js +203 -77
  5. package/dist/HomeKitHistory.js +1 -1
  6. package/dist/config.js +207 -0
  7. package/dist/devices.js +118 -0
  8. package/dist/index.js +2 -1
  9. package/dist/nexustalk.js +46 -48
  10. package/dist/{camera.js → plugins/camera.js} +216 -241
  11. package/dist/{doorbell.js → plugins/doorbell.js} +32 -30
  12. package/dist/plugins/floodlight.js +91 -0
  13. package/dist/plugins/heatlink.js +17 -0
  14. package/dist/{protect.js → plugins/protect.js} +26 -43
  15. package/dist/{tempsensor.js → plugins/tempsensor.js} +15 -19
  16. package/dist/{thermostat.js → plugins/thermostat.js} +426 -383
  17. package/dist/{weather.js → plugins/weather.js} +26 -60
  18. package/dist/protobuf/nest/services/apigateway.proto +31 -1
  19. package/dist/protobuf/nest/trait/firmware.proto +207 -89
  20. package/dist/protobuf/nest/trait/hvac.proto +1052 -312
  21. package/dist/protobuf/nest/trait/located.proto +51 -8
  22. package/dist/protobuf/nest/trait/network.proto +366 -36
  23. package/dist/protobuf/nest/trait/occupancy.proto +145 -17
  24. package/dist/protobuf/nest/trait/product/protect.proto +57 -43
  25. package/dist/protobuf/nest/trait/resourcedirectory.proto +8 -0
  26. package/dist/protobuf/nest/trait/sensor.proto +7 -1
  27. package/dist/protobuf/nest/trait/service.proto +3 -1
  28. package/dist/protobuf/nest/trait/structure.proto +60 -14
  29. package/dist/protobuf/nest/trait/ui.proto +41 -1
  30. package/dist/protobuf/nest/trait/user.proto +6 -1
  31. package/dist/protobuf/nest/trait/voiceassistant.proto +2 -1
  32. package/dist/protobuf/nestlabs/eventingapi/v1.proto +20 -1
  33. package/dist/protobuf/root.proto +1 -0
  34. package/dist/protobuf/wdl.proto +18 -2
  35. package/dist/protobuf/weave/common.proto +2 -1
  36. package/dist/protobuf/weave/trait/heartbeat.proto +41 -1
  37. package/dist/protobuf/weave/trait/power.proto +1 -0
  38. package/dist/protobuf/weave/trait/security.proto +10 -1
  39. package/dist/streamer.js +74 -78
  40. package/dist/system.js +1213 -1264
  41. package/dist/webrtc.js +39 -34
  42. package/package.json +11 -11
  43. package/dist/floodlight.js +0 -97
@@ -1,7 +1,6 @@
1
1
  // Nest Cameras
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 2025/03/19
5
4
  // Mark Hulskamp
6
5
  'use strict';
7
6
 
@@ -18,18 +17,28 @@ import path from 'node:path';
18
17
  import { fileURLToPath } from 'node:url';
19
18
 
20
19
  // Define our modules
21
- import HomeKitDevice from './HomeKitDevice.js';
22
- import NexusTalk from './nexustalk.js';
23
- import WebRTC from './webrtc.js';
24
-
25
- const CAMERAOFFLINEJPGFILE = 'Nest_camera_offline.jpg'; // Camera offline jpg image file
26
- const CAMERAOFFJPGFILE = 'Nest_camera_off.jpg'; // Camera video off jpg image file
27
- const CAMERATRANSFERJPGFILE = 'Nest_camera_transfer.jpg'; // Camera transferring jpg image file
20
+ import HomeKitDevice from '../HomeKitDevice.js';
21
+ import NexusTalk from '../nexustalk.js';
22
+ import WebRTC from '../webrtc.js';
23
+
24
+ const CAMERA_RESOURCE = {
25
+ OFFLINE: 'Nest_camera_offline.jpg',
26
+ OFF: 'Nest_camera_off.jpg',
27
+ TRANSFER: 'Nest_camera_transfer.jpg',
28
+ };
28
29
  const MP4BOX = 'mp4box'; // MP4 box fragement event for HKSV recording
29
- const SNAPSHOTCACHETIMEOUT = 30000; // Timeout for retaining snapshot image (in milliseconds)
30
+ const SNAPSHOT_CACHE_TIMEOUT = 30000; // Timeout for retaining snapshot image (in milliseconds)
31
+ const STREAMING_PROTOCOL = {
32
+ WEBRTC: 'PROTOCOL_WEBRTC',
33
+ NEXUSTALK: 'PROTOCOL_NEXUSTALK',
34
+ };
35
+ const RESOURCE_PATH = '../res';
30
36
  const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
31
37
 
32
38
  export default class NestCamera extends HomeKitDevice {
39
+ static TYPE = 'Camera';
40
+ static VERSION = '2025.06.15';
41
+
33
42
  controller = undefined; // HomeKit Camera/Doorbell controller service
34
43
  streamer = undefined; // Streamer object for live/recording stream
35
44
  motionServices = undefined; // Object of Camera/Doorbell motion sensor(s)
@@ -51,27 +60,25 @@ export default class NestCamera extends HomeKitDevice {
51
60
  constructor(accessory, api, log, eventEmitter, deviceData) {
52
61
  super(accessory, api, log, eventEmitter, deviceData);
53
62
 
54
- // buffer for camera offline jpg image
55
- let imageFile = path.resolve(__dirname + '/res/' + CAMERAOFFLINEJPGFILE);
56
- if (fs.existsSync(imageFile) === true) {
57
- this.#cameraOfflineImage = fs.readFileSync(imageFile);
58
- }
59
-
60
- // buffer for camera stream off jpg image
61
- imageFile = path.resolve(__dirname + '/res/' + CAMERAOFFJPGFILE);
62
- if (fs.existsSync(imageFile) === true) {
63
- this.#cameraVideoOffImage = fs.readFileSync(imageFile);
64
- }
63
+ // Load supporrt image files as required
64
+ const loadImageIfExists = (filename, label) => {
65
+ let buffer = undefined;
66
+ let file = path.resolve(__dirname, RESOURCE_PATH, filename);
67
+ if (fs.existsSync(file) === true) {
68
+ buffer = fs.readFileSync(file);
69
+ } else {
70
+ this.log?.warn?.('Failed to load %s image resource for "%s"', label, this.deviceData.description);
71
+ }
72
+ return buffer;
73
+ };
65
74
 
66
- // buffer for camera transferring jpg image
67
- imageFile = path.resolve(__dirname + '/res/' + CAMERATRANSFERJPGFILE);
68
- if (fs.existsSync(imageFile) === true) {
69
- this.#cameraTransferringImage = fs.readFileSync(imageFile);
70
- }
75
+ this.#cameraOfflineImage = loadImageIfExists(CAMERA_RESOURCE.OFFLINE, 'offline');
76
+ this.#cameraVideoOffImage = loadImageIfExists(CAMERA_RESOURCE.OFF, 'video off');
77
+ this.#cameraTransferringImage = loadImageIfExists(CAMERA_RESOURCE.TRANSFER, 'transferring');
71
78
  }
72
79
 
73
80
  // Class functions
74
- addServices(hapController = this.hap.CameraController) {
81
+ setupDevice(hapController = this.hap.CameraController) {
75
82
  // Setup motion services
76
83
  if (this.motionServices === undefined) {
77
84
  this.createCameraMotionServices();
@@ -92,19 +99,13 @@ export default class NestCamera extends HomeKitDevice {
92
99
  if (this.operatingModeService === undefined) {
93
100
  // Add in operating mode service for a non-hksv camera/doorbell
94
101
  // Allow us to change things such as night vision, camera indicator etc within HomeKit for those also :-)
95
- this.operatingModeService = this.accessory.getService(this.hap.Service.CameraOperatingMode);
96
- if (this.operatingModeService === undefined) {
97
- this.operatingModeService = this.accessory.addService(this.hap.Service.CameraOperatingMode, '', 1);
98
- }
102
+ this.operatingModeService = this.addHKService(this.hap.Service.CameraOperatingMode, '', 1);
99
103
  }
100
104
 
101
- // Setup set callbacks for characteristics
102
- if (this.operatingModeService !== undefined) {
103
- if (this.deviceData.has_statusled === true) {
104
- if (this.operatingModeService.testCharacteristic(this.hap.Characteristic.CameraOperatingModeIndicator) === false) {
105
- this.operatingModeService.addOptionalCharacteristic(this.hap.Characteristic.CameraOperatingModeIndicator);
106
- }
107
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.CameraOperatingModeIndicator).onSet((value) => {
105
+ // Setup set characteristics
106
+ if (this.deviceData?.has_statusled === true) {
107
+ this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.CameraOperatingModeIndicator, {
108
+ onSet: (value) => {
108
109
  // 0 = auto, 1 = low, 2 = high
109
110
  // We'll use auto mode for led on and low for led off
110
111
  if (
@@ -112,43 +113,33 @@ export default class NestCamera extends HomeKitDevice {
112
113
  (value === false && this.deviceData.statusled_brightness !== 1)
113
114
  ) {
114
115
  this.set({ uuid: this.deviceData.nest_google_uuid, statusled_brightness: value === true ? 0 : 1 });
115
- if (this?.log?.info) {
116
- this.log.info('Recording status LED on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
117
- }
116
+ this?.log?.info?.('Recording status LED on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
118
117
  }
119
- });
120
-
121
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.CameraOperatingModeIndicator).onGet(() => {
118
+ },
119
+ onGet: () => {
122
120
  return this.deviceData.statusled_brightness !== 1;
123
- });
124
- }
125
-
126
- if (this.deviceData.has_irled === true) {
127
- if (this.operatingModeService.testCharacteristic(this.hap.Characteristic.NightVision) === false) {
128
- this.operatingModeService.addOptionalCharacteristic(this.hap.Characteristic.NightVision);
129
- }
121
+ },
122
+ });
123
+ }
130
124
 
131
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.NightVision).onSet((value) => {
125
+ if (this.deviceData?.has_irled === true) {
126
+ this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.NightVision, {
127
+ onSet: (value) => {
132
128
  // only change IRLed status value if different than on-device
133
129
  if ((value === false && this.deviceData.irled_enabled === true) || (value === true && this.deviceData.irled_enabled === false)) {
134
130
  this.set({ uuid: this.deviceData.nest_google_uuid, irled_enabled: value === true ? 'auto_on' : 'always_off' });
135
131
 
136
- if (this?.log?.info) {
137
- this.log.info('Night vision on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
138
- }
132
+ this?.log?.info?.('Night vision on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
139
133
  }
140
- });
141
-
142
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.NightVision).onGet(() => {
134
+ },
135
+ onGet: () => {
143
136
  return this.deviceData.irled_enabled;
144
- });
145
- }
146
-
147
- if (this.operatingModeService.testCharacteristic(this.hap.Characteristic.ManuallyDisabled) === false) {
148
- this.operatingModeService.addOptionalCharacteristic(this.hap.Characteristic.ManuallyDisabled);
149
- }
137
+ },
138
+ });
139
+ }
150
140
 
151
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.ManuallyDisabled).onSet((value) => {
141
+ this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.ManuallyDisabled, {
142
+ onSet: (value) => {
152
143
  if (value !== this.operatingModeService.getCharacteristic(this.hap.Characteristic.ManuallyDisabled).value) {
153
144
  // Make sure only updating status if HomeKit value *actually changes*
154
145
  if (
@@ -157,35 +148,31 @@ export default class NestCamera extends HomeKitDevice {
157
148
  ) {
158
149
  // Camera state does not reflect requested state, so fix
159
150
  this.set({ uuid: this.deviceData.nest_google_uuid, streaming_enabled: value === false ? true : false });
160
- if (this?.log?.info) {
161
- this.log.info('Camera on "%s" was turned', this.deviceData.description, value === false ? 'on' : 'off');
162
- }
151
+ this?.log?.info?.('Camera on "%s" was turned', this.deviceData.description, value === false ? 'on' : 'off');
163
152
  }
164
153
  }
165
- });
166
-
167
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.ManuallyDisabled).onGet(() => {
154
+ },
155
+ onGet: () => {
168
156
  return this.deviceData.streaming_enabled === false
169
157
  ? this.hap.Characteristic.ManuallyDisabled.DISABLED
170
158
  : this.hap.Characteristic.ManuallyDisabled.ENABLED;
171
- });
172
-
173
- if (this.deviceData.has_video_flip === true) {
174
- if (this.operatingModeService.testCharacteristic(this.hap.Characteristic.ImageRotation) === false) {
175
- this.operatingModeService.addOptionalCharacteristic(this.hap.Characteristic.ImageRotation);
176
- }
159
+ },
160
+ });
177
161
 
178
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.ImageRotation).onGet(() => {
162
+ if (this.deviceData?.has_video_flip === true) {
163
+ this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.ImageRotation, {
164
+ onGet: () => {
179
165
  return this.deviceData.video_flipped === true ? 180 : 0;
180
- });
181
- }
166
+ },
167
+ });
182
168
  }
183
169
 
184
- if (this.controller?.recordingManagement?.recordingManagementService !== undefined) {
185
- if (this.deviceData.has_microphone === true) {
186
- this.controller.recordingManagement.recordingManagementService
187
- .getCharacteristic(this.hap.Characteristic.RecordingAudioActive)
188
- .onSet((value) => {
170
+ if (this.controller?.recordingManagement?.recordingManagementService !== undefined && this.deviceData.has_microphone === true) {
171
+ this.addHKCharacteristic(
172
+ this.controller.recordingManagement.recordingManagementService,
173
+ this.hap.Characteristic.RecordingAudioActive,
174
+ {
175
+ onSet: (value) => {
189
176
  if (
190
177
  (this.deviceData.audio_enabled === true && value === this.hap.Characteristic.RecordingAudioActive.DISABLE) ||
191
178
  (this.deviceData.audio_enabled === false && value === this.hap.Characteristic.RecordingAudioActive.ENABLE)
@@ -194,42 +181,37 @@ export default class NestCamera extends HomeKitDevice {
194
181
  uuid: this.deviceData.nest_google_uuid,
195
182
  audio_enabled: value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? true : false,
196
183
  });
197
- if (this?.log?.info) {
198
- this.log.info(
199
- 'Audio recording on "%s" was turned',
200
- this.deviceData.description,
201
- value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? 'on' : 'off',
202
- );
203
- }
184
+ this?.log?.info?.(
185
+ 'Audio recording on "%s" was turned',
186
+ this.deviceData.description,
187
+ value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? 'on' : 'off',
188
+ );
204
189
  }
205
- });
206
-
207
- this.controller.recordingManagement.recordingManagementService
208
- .getCharacteristic(this.hap.Characteristic.RecordingAudioActive)
209
- .onGet(() => {
190
+ },
191
+ onGet: () => {
210
192
  return this.deviceData.audio_enabled === true
211
193
  ? this.hap.Characteristic.RecordingAudioActive.ENABLE
212
194
  : this.hap.Characteristic.RecordingAudioActive.DISABLE;
213
- });
214
- }
195
+ },
196
+ },
197
+ );
215
198
  }
216
199
 
217
200
  if (this.deviceData.migrating === true) {
218
201
  // Migration happening between Nest <-> Google Home apps
219
- this?.log?.warn && this.log.warn('Migration between Nest <-> Google Home apps is underway for "%s"', this.deviceData.description);
202
+ this?.log?.warn?.('Migration between Nest <-> Google Home apps is underway for "%s"', this.deviceData.description);
220
203
  }
221
204
 
222
205
  if (
223
- (this.deviceData.streaming_protocols.includes('PROTOCOL_WEBRTC') === false &&
224
- this.deviceData.streaming_protocols.includes('PROTOCOL_NEXUSTALK') === false) ||
225
- (this.deviceData.streaming_protocols.includes('PROTOCOL_WEBRTC') === true && WebRTC === undefined) ||
226
- (this.deviceData.streaming_protocols.includes('PROTOCOL_NEXUSTALK') === true && NexusTalk === undefined)
206
+ (this.deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.WEBRTC) === false &&
207
+ this.deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.NEXUSTALK) === false) ||
208
+ (this.deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.WEBRTC) === true && WebRTC === undefined) ||
209
+ (this.deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.NEXUSTALK) === true && NexusTalk === undefined)
227
210
  ) {
228
- this?.log?.error &&
229
- this.log.error(
230
- 'No suitable streaming protocol is present for "%s". Streaming and recording will be unavailable',
231
- this.deviceData.description,
232
- );
211
+ this?.log?.error?.(
212
+ 'No suitable streaming protocol is present for "%s". Streaming and recording will be unavailable',
213
+ this.deviceData.description,
214
+ );
233
215
  }
234
216
 
235
217
  // Setup linkage to EveHome app if configured todo so
@@ -243,17 +225,13 @@ export default class NestCamera extends HomeKitDevice {
243
225
  });
244
226
  }
245
227
 
246
- // Create extra details for output
247
- let postSetupDetails = [];
228
+ // Extra setup details for output
248
229
  this.deviceData.hksv === true &&
249
- postSetupDetails.push(
250
- 'HomeKit Secure Video support' + (this.streamer?.isBuffering() === true ? ' and recording buffer started' : ''),
251
- );
252
- this.deviceData.localAccess === true && postSetupDetails.push('Local access');
253
- return postSetupDetails;
230
+ this.postSetupDetail('HomeKit Secure Video support' + (this.streamer?.isBuffering() === true ? ' and recording buffer started' : ''));
231
+ this.deviceData.localAccess === true && this.postSetupDetail('Local access');
254
232
  }
255
233
 
256
- removeServices() {
234
+ removeDevice() {
257
235
  // Clean up our camera object since this device is being removed
258
236
  clearTimeout(this.motionTimer);
259
237
  clearTimeout(this.personTimer);
@@ -298,11 +276,10 @@ export default class NestCamera extends HomeKitDevice {
298
276
  // https://github.com/hjdhjd/homebridge-unifi-protect/blob/eee6a4e379272b659baa6c19986d51f5bf2cbbbc/src/protect-ffmpeg-record.ts
299
277
  async *handleRecordingStreamRequest(sessionID) {
300
278
  if (this.deviceData?.ffmpeg?.binary === undefined) {
301
- this?.log?.warn &&
302
- this.log.warn(
303
- 'Received request to start recording for "%s" however we do not have an ffmpeg binary present',
304
- this.deviceData.description,
305
- );
279
+ this?.log?.warn?.(
280
+ 'Received request to start recording for "%s" however we do not have an ffmpeg binary present',
281
+ this.deviceData.description,
282
+ );
306
283
  return;
307
284
  }
308
285
 
@@ -312,16 +289,15 @@ export default class NestCamera extends HomeKitDevice {
312
289
  ) {
313
290
  // Should only be recording if motion detected.
314
291
  // Sometimes when starting up, HAP-nodeJS or HomeKit triggers this even when motion isn't occuring
315
- this?.log?.debug && this.log.debug('Received request to commence recording for "%s" however we have not detected any motion');
292
+ this?.log?.debug?.('Received request to commence recording for "%s" however we have not detected any motion');
316
293
  return;
317
294
  }
318
295
 
319
296
  if (this.streamer === undefined) {
320
- this?.log?.error &&
321
- this.log.error(
322
- 'Received request to start recording for "%s" however we do not any associated streaming protocol support',
323
- this.deviceData.description,
324
- );
297
+ this?.log?.error?.(
298
+ 'Received request to start recording for "%s" however we do not any associated streaming protocol support',
299
+ this.deviceData.description,
300
+ );
325
301
  return;
326
302
  }
327
303
 
@@ -410,12 +386,11 @@ export default class NestCamera extends HomeKitDevice {
410
386
  // Start our ffmpeg recording process and stream from our streamer
411
387
  // video is pipe #1
412
388
  // audio is pipe #3 if including audio
413
- this?.log?.debug &&
414
- this.log.debug(
415
- 'ffmpeg process for recording stream from "%s" will be called using the following commandline',
416
- this.deviceData.description,
417
- commandLine.join(' ').toString(),
418
- );
389
+ this?.log?.debug?.(
390
+ 'ffmpeg process for recording stream from "%s" will be called using the following commandline',
391
+ this.deviceData.description,
392
+ commandLine.join(' ').toString(),
393
+ );
419
394
  let ffmpegRecording = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
420
395
  env: process.env,
421
396
  stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
@@ -455,8 +430,7 @@ export default class NestCamera extends HomeKitDevice {
455
430
 
456
431
  ffmpegRecording.on('exit', (code, signal) => {
457
432
  if (signal !== 'SIGKILL' || signal === null) {
458
- this?.log?.error &&
459
- this.log.error('ffmpeg recording process for "%s" stopped unexpectedly. Exit code was "%s"', this.deviceData.description, code);
433
+ this?.log?.error?.('ffmpeg recording process for "%s" stopped unexpectedly. Exit code was "%s"', this.deviceData.description, code);
460
434
  }
461
435
 
462
436
  if (this.#hkSessions?.[sessionID] !== undefined) {
@@ -473,7 +447,7 @@ export default class NestCamera extends HomeKitDevice {
473
447
  ffmpegRecording.stderr.on('data', (data) => {
474
448
  if (data.toString().includes('frame=') === false && this.deviceData?.ffmpeg?.debug === true) {
475
449
  // Monitor ffmpeg output
476
- this?.log?.debug && this.log.debug(data.toString());
450
+ this?.log?.debug?.(data.toString());
477
451
  }
478
452
  });
479
453
 
@@ -486,8 +460,7 @@ export default class NestCamera extends HomeKitDevice {
486
460
  this.#hkSessions[sessionID].eventEmitter = eventEmitter;
487
461
  this.#hkSessions[sessionID].ffmpeg = ffmpegRecording; // Store ffmpeg process ID
488
462
 
489
- this?.log?.info &&
490
- this.log.info('Started recording from "%s" %s', this.deviceData.description, includeAudio === false ? 'without audio' : '');
463
+ this?.log?.info?.('Started recording from "%s" %s', this.deviceData.description, includeAudio === false ? 'without audio' : '');
491
464
 
492
465
  // Loop generating MOOF/MDAT box pairs for HomeKit Secure Video.
493
466
  // HAP-NodeJS cancels this async generator function when recording completes also
@@ -539,14 +512,13 @@ export default class NestCamera extends HomeKitDevice {
539
512
 
540
513
  // Log recording finished messages depending on reason
541
514
  if (closeReason === this.hap.HDSProtocolSpecificErrorReason.NORMAL) {
542
- this?.log?.info && this.log.info('Completed recording from "%s"', this.deviceData.description);
515
+ this?.log?.info?.('Completed recording from "%s"', this.deviceData.description);
543
516
  } else {
544
- this?.log?.warn &&
545
- this.log.warn(
546
- 'Recording from "%s" completed with error. Reason was "%s"',
547
- this.deviceData.description,
548
- this.hap.HDSProtocolSpecificErrorReason[closeReason],
549
- );
517
+ this?.log?.warn?.(
518
+ 'Recording from "%s" completed with error. Reason was "%s"',
519
+ this.deviceData.description,
520
+ this.hap.HDSProtocolSpecificErrorReason[closeReason],
521
+ );
550
522
  }
551
523
  }
552
524
 
@@ -555,13 +527,13 @@ export default class NestCamera extends HomeKitDevice {
555
527
  // Start a buffering stream for this camera/doorbell. Ensures motion captures all video on motion trigger
556
528
  // Required due to data delays by on prem Nest to cloud to HomeKit accessory to iCloud etc
557
529
  // Make sure have appropriate bandwidth!!!
558
- this?.log?.info && this.log.info('Recording was turned on for "%s"', this.deviceData.description);
530
+ this?.log?.info?.('Recording was turned on for "%s"', this.deviceData.description);
559
531
  this.streamer.startBuffering();
560
532
  }
561
533
 
562
534
  if (enableRecording === false && this.streamer?.isBuffering() === true) {
563
535
  this.streamer.stopBuffering();
564
- this?.log?.warn && this.log.warn('Recording was turned off for "%s"', this.deviceData.description);
536
+ this?.log?.warn?.('Recording was turned off for "%s"', this.deviceData.description);
565
537
  }
566
538
  }
567
539
 
@@ -586,7 +558,7 @@ export default class NestCamera extends HomeKitDevice {
586
558
  clearTimeout(this.snapshotTimer);
587
559
  this.snapshotTimer = setTimeout(() => {
588
560
  this.lastSnapshotImage = undefined;
589
- }, SNAPSHOTCACHETIMEOUT);
561
+ }, SNAPSHOT_CACHE_TIMEOUT);
590
562
  }
591
563
  }
592
564
 
@@ -688,20 +660,18 @@ export default class NestCamera extends HomeKitDevice {
688
660
  // called when HomeKit asks to start/stop/reconfigure a camera/doorbell live stream
689
661
  if (request.type === this.hap.StreamRequestTypes.START && this.streamer === undefined) {
690
662
  // We have no streamer object configured, so cannot do live streams!!
691
- this?.log?.error &&
692
- this.log.error(
693
- 'Received request to start live video for "%s" however we do not any associated streaming protocol support',
694
- this.deviceData.description,
695
- );
663
+ this?.log?.error?.(
664
+ 'Received request to start live video for "%s" however we do not any associated streaming protocol support',
665
+ this.deviceData.description,
666
+ );
696
667
  }
697
668
 
698
669
  if (request.type === this.hap.StreamRequestTypes.START && this.deviceData?.ffmpeg?.binary === undefined) {
699
670
  // No ffmpeg binary present, so cannot do live streams!!
700
- this?.log?.warn &&
701
- this.log.warn(
702
- 'Received request to start live video for "%s" however we do not have an ffmpeg binary present',
703
- this.deviceData.description,
704
- );
671
+ this?.log?.warn?.(
672
+ 'Received request to start live video for "%s" however we do not have an ffmpeg binary present',
673
+ this.deviceData.description,
674
+ );
705
675
  }
706
676
 
707
677
  if (
@@ -802,12 +772,11 @@ export default class NestCamera extends HomeKitDevice {
802
772
  // Start our ffmpeg streaming process and stream from our streamer
803
773
  // video is pipe #1
804
774
  // audio is pipe #3 if including audio
805
- this?.log?.debug &&
806
- this.log.debug(
807
- 'ffmpeg process for live streaming from "%s" will be called using the following commandline',
808
- this.deviceData.description,
809
- commandLine.join(' ').toString(),
810
- );
775
+ this?.log?.debug?.(
776
+ 'ffmpeg process for live streaming from "%s" will be called using the following commandline',
777
+ this.deviceData.description,
778
+ commandLine.join(' ').toString(),
779
+ );
811
780
  let ffmpegStreaming = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
812
781
  env: process.env,
813
782
  stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
@@ -815,12 +784,11 @@ export default class NestCamera extends HomeKitDevice {
815
784
 
816
785
  ffmpegStreaming.on('exit', (code, signal) => {
817
786
  if (signal !== 'SIGKILL' || signal === null) {
818
- this?.log?.error &&
819
- this.log.error(
820
- 'ffmpeg video/audio live streaming process for "%s" stopped unexpectedly. Exit code was "%s"',
821
- this.deviceData.description,
822
- code,
823
- );
787
+ this?.log?.error?.(
788
+ 'ffmpeg video/audio live streaming process for "%s" stopped unexpectedly. Exit code was "%s"',
789
+ this.deviceData.description,
790
+ code,
791
+ );
824
792
 
825
793
  // Clean up or streaming request, but calling it again with a 'STOP' reques
826
794
  this.handleStreamRequest({ type: this.hap.StreamRequestTypes.STOP, sessionID: request.sessionID }, null);
@@ -831,7 +799,7 @@ export default class NestCamera extends HomeKitDevice {
831
799
  ffmpegStreaming.stderr.on('data', (data) => {
832
800
  if (data.toString().includes('frame=') === false && this.deviceData?.ffmpeg?.debug === true) {
833
801
  // Monitor ffmpeg output
834
- this?.log?.debug && this.log.debug(data.toString());
802
+ this?.log?.debug?.(data.toString());
835
803
  }
836
804
  });
837
805
 
@@ -894,24 +862,22 @@ export default class NestCamera extends HomeKitDevice {
894
862
 
895
863
  commandLine.push('-f data pipe:1');
896
864
 
897
- this?.log?.debug &&
898
- this.log.debug(
899
- 'ffmpeg process for talkback on "%s" will be called using the following commandline',
900
- this.deviceData.description,
901
- commandLine.join(' ').toString(),
902
- );
865
+ this?.log?.debug?.(
866
+ 'ffmpeg process for talkback on "%s" will be called using the following commandline',
867
+ this.deviceData.description,
868
+ commandLine.join(' ').toString(),
869
+ );
903
870
  ffmpegAudioTalkback = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
904
871
  env: process.env,
905
872
  });
906
873
 
907
874
  ffmpegAudioTalkback.on('exit', (code, signal) => {
908
875
  if (signal !== 'SIGKILL' || signal === null) {
909
- this?.log?.error &&
910
- this.log.error(
911
- 'ffmpeg audio talkback streaming process for "%s" stopped unexpectedly. Exit code was "%s"',
912
- this.deviceData.description,
913
- code,
914
- );
876
+ this?.log?.error?.(
877
+ 'ffmpeg audio talkback streaming process for "%s" stopped unexpectedly. Exit code was "%s"',
878
+ this.deviceData.description,
879
+ code,
880
+ );
915
881
 
916
882
  // Clean up or streaming request, but calling it again with a 'STOP' request
917
883
  this.handleStreamRequest({ type: this.hap.StreamRequestTypes.STOP, sessionID: request.sessionID }, null);
@@ -927,7 +893,7 @@ export default class NestCamera extends HomeKitDevice {
927
893
  ffmpegAudioTalkback.stderr.on('data', (data) => {
928
894
  if (data.toString().includes('frame=') === false && this.deviceData?.ffmpeg?.debug === true) {
929
895
  // Monitor ffmpeg output
930
- this?.log?.debug && this.log.debug(data.toString());
896
+ this?.log?.debug?.(data.toString());
931
897
  }
932
898
  });
933
899
 
@@ -971,12 +937,11 @@ export default class NestCamera extends HomeKitDevice {
971
937
  ffmpegAudioTalkback.stdin.end();
972
938
  }
973
939
 
974
- this?.log?.info &&
975
- this.log.info(
976
- 'Live stream started on "%s" %s',
977
- this.deviceData.description,
978
- ffmpegAudioTalkback?.stdout ? 'with two-way audio' : '',
979
- );
940
+ this?.log?.info?.(
941
+ 'Live stream started on "%s" %s',
942
+ this.deviceData.description,
943
+ ffmpegAudioTalkback?.stdout ? 'with two-way audio' : '',
944
+ );
980
945
 
981
946
  // Start the appropriate streamer
982
947
  this.streamer !== undefined &&
@@ -1010,11 +975,11 @@ export default class NestCamera extends HomeKitDevice {
1010
975
 
1011
976
  delete this.#hkSessions[request.sessionID];
1012
977
 
1013
- this?.log?.info && this.log.info('Live stream stopped from "%s"', this.deviceData.description);
978
+ this?.log?.info?.('Live stream stopped from "%s"', this.deviceData.description);
1014
979
  }
1015
980
 
1016
981
  if (request.type === this.hap.StreamRequestTypes.RECONFIGURE && typeof this.#hkSessions[request.sessionID] === 'object') {
1017
- this?.log?.debug && this.log.debug('Unsupported reconfiguration request for live stream on "%s"', this.deviceData.description);
982
+ this?.log?.debug?.('Unsupported reconfiguration request for live stream on "%s"', this.deviceData.description);
1018
983
  }
1019
984
 
1020
985
  if (typeof callback === 'function') {
@@ -1022,32 +987,32 @@ export default class NestCamera extends HomeKitDevice {
1022
987
  }
1023
988
  }
1024
989
 
1025
- updateServices(deviceData) {
990
+ updateDevice(deviceData) {
1026
991
  if (typeof deviceData !== 'object' || this.controller === undefined) {
1027
992
  return;
1028
993
  }
1029
994
 
1030
995
  if (this.deviceData.migrating === false && deviceData.migrating === true) {
1031
996
  // Migration happening between Nest <-> Google Home apps. We'll stop any active streams, close the current streaming object
1032
- this?.log?.warn && this.log.warn('Migration between Nest <-> Google Home apps has started for "%s"', deviceData.description);
997
+ this?.log?.warn?.('Migration between Nest <-> Google Home apps has started for "%s"', deviceData.description);
1033
998
  this.streamer !== undefined && this.streamer.stopEverything();
1034
999
  this.streamer = undefined;
1035
1000
  }
1036
1001
 
1037
1002
  if (this.deviceData.migrating === true && deviceData.migrating === false) {
1038
1003
  // Migration has completed between Nest <-> Google Home apps
1039
- this?.log?.success && this.log.success('Migration between Nest <-> Google Home apps has completed for "%s"', deviceData.description);
1004
+ this?.log?.success?.('Migration between Nest <-> Google Home apps has completed for "%s"', deviceData.description);
1040
1005
  }
1041
1006
 
1042
1007
  // Handle case of changes in streaming protocols OR just finished migration between Nest <-> Google Home apps
1043
1008
  if (this.streamer === undefined && deviceData.migrating === false) {
1044
1009
  if (JSON.stringify(deviceData.streaming_protocols) !== JSON.stringify(this.deviceData.streaming_protocols)) {
1045
- this?.log?.warn && this.log.warn('Available streaming protocols have changed for "%s"', deviceData.description);
1010
+ this?.log?.warn?.('Available streaming protocols have changed for "%s"', deviceData.description);
1046
1011
  this.streamer !== undefined && this.streamer.stopEverything();
1047
1012
  this.streamer = undefined;
1048
1013
  }
1049
- if (deviceData.streaming_protocols.includes('PROTOCOL_WEBRTC') === true && WebRTC !== undefined) {
1050
- this?.log?.debug && this.log.debug('Using WebRTC streamer for "%s"', deviceData.description);
1014
+ if (deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.WEBRTC) === true && WebRTC !== undefined) {
1015
+ this?.log?.debug?.('Using WebRTC streamer for "%s"', deviceData.description);
1051
1016
  this.streamer = new WebRTC(deviceData, {
1052
1017
  log: this.log,
1053
1018
  buffer:
@@ -1058,8 +1023,8 @@ export default class NestCamera extends HomeKitDevice {
1058
1023
  });
1059
1024
  }
1060
1025
 
1061
- if (deviceData.streaming_protocols.includes('PROTOCOL_NEXUSTALK') === true && NexusTalk !== undefined) {
1062
- this?.log?.debug && this.log.debug('Using NexusTalk streamer for "%s"', deviceData.description);
1026
+ if (deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.NEXUSTALK) === true && NexusTalk !== undefined) {
1027
+ this?.log?.debug?.('Using NexusTalk streamer for "%s"', deviceData.description);
1063
1028
  this.streamer = new NexusTalk(deviceData, {
1064
1029
  log: this.log,
1065
1030
  buffer:
@@ -1072,22 +1037,26 @@ export default class NestCamera extends HomeKitDevice {
1072
1037
  }
1073
1038
 
1074
1039
  // Check to see if any activity zones were added for both non-HKSV and HKSV enabled devices
1075
- deviceData.activity_zones.forEach((zone) => {
1076
- if (
1077
- JSON.stringify(deviceData.activity_zones) !== JSON.stringify(this.deviceData.activity_zones) &&
1078
- (this.deviceData.hksv === false || (this.deviceData.hksv === true && zone.id === 1))
1079
- ) {
1080
- if (this.motionServices?.[zone.id]?.service === undefined) {
1081
- // Zone doesn't have an associated motion sensor, so add one
1082
- let tempService = this.accessory.addService(this.hap.Service.MotionSensor, zone.id === 1 ? '' : zone.name, zone.id);
1083
- if (tempService.testCharacteristic(this.hap.Characteristic.Active) === false) {
1084
- tempService.addCharacteristic(this.hap.Characteristic.Active);
1040
+ if (
1041
+ Array.isArray(deviceData.activity_zones) === true &&
1042
+ JSON.stringify(deviceData.activity_zones) !== JSON.stringify(this.deviceData.activity_zones)
1043
+ ) {
1044
+ deviceData.activity_zones.forEach((zone) => {
1045
+ if (this.deviceData.hksv === false || (this.deviceData.hksv === true && zone.id === 1)) {
1046
+ if (this.motionServices?.[zone.id]?.service === undefined) {
1047
+ // Zone doesn't have an associated motion sensor, so add one
1048
+ let zoneName = zone.id === 1 ? '' : zone.name;
1049
+ let tempService = this.addHKService(this.hap.Service.MotionSensor, zoneName, zone.id);
1050
+
1051
+ this.addHKCharacteristic(tempService, this.hap.Characteristic.Active);
1052
+ tempService.updateCharacteristic(this.hap.Characteristic.Name, zoneName);
1053
+ tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
1054
+
1055
+ this.motionServices[zone.id] = { service: tempService, timer: undefined };
1085
1056
  }
1086
- tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
1087
- this.motionServices[zone.id] = { service: tempService, timer: undefined };
1088
1057
  }
1089
- }
1090
- });
1058
+ });
1059
+ }
1091
1060
 
1092
1061
  // Check to see if any activity zones were removed for both non-HKSV and HKSV enabled devices
1093
1062
  // We'll also update the online status of the camera in the motion service here
@@ -1098,12 +1067,15 @@ export default class NestCamera extends HomeKitDevice {
1098
1067
  deviceData.online === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
1099
1068
  );
1100
1069
 
1101
- if (JSON.stringify(deviceData.activity_zones) !== JSON.stringify(this.deviceData.activity_zones) && zoneID !== 1) {
1102
- if (deviceData.activity_zones.findIndex(({ id }) => id === zoneID) === -1) {
1103
- // Motion service we created doesn't appear in zone list anymore, so assume deleted
1104
- this.accessory.removeService(service.service);
1105
- delete this.motionServices[zoneID];
1106
- }
1070
+ // Handle deleted zones (excluding zone ID 1 for HKSV)
1071
+ if (
1072
+ zoneID !== '1' &&
1073
+ Array.isArray(deviceData.activity_zones) === true &&
1074
+ deviceData.activity_zones.findIndex(({ id }) => id === Number(zoneID)) === -1
1075
+ ) {
1076
+ // Motion service we created doesn't appear in zone list anymore, so assume deleted
1077
+ this.accessory.removeService(service.service);
1078
+ delete this.motionServices[zoneID];
1107
1079
  }
1108
1080
  });
1109
1081
 
@@ -1116,7 +1088,7 @@ export default class NestCamera extends HomeKitDevice {
1116
1088
  : this.hap.Characteristic.ManuallyDisabled.ENABLED,
1117
1089
  );
1118
1090
 
1119
- if (deviceData.has_statusled === true) {
1091
+ if (deviceData?.has_statusled === true) {
1120
1092
  // Set camera recording indicator. This cannot be turned off on Nest Cameras/Doorbells
1121
1093
  // 0 = auto
1122
1094
  // 1 = low
@@ -1127,12 +1099,12 @@ export default class NestCamera extends HomeKitDevice {
1127
1099
  );
1128
1100
  }
1129
1101
 
1130
- if (deviceData.has_irled === true) {
1102
+ if (deviceData?.has_irled === true) {
1131
1103
  // Set nightvision status in HomeKit
1132
1104
  this.operatingModeService.updateCharacteristic(this.hap.Characteristic.NightVision, deviceData.irled_enabled);
1133
1105
  }
1134
1106
 
1135
- if (deviceData.has_video_flip === true) {
1107
+ if (deviceData?.has_video_flip === true) {
1136
1108
  // Update image flip status
1137
1109
  this.operatingModeService.updateCharacteristic(this.hap.Characteristic.ImageRotation, deviceData.video_flipped === true ? 180 : 0);
1138
1110
  }
@@ -1183,7 +1155,7 @@ export default class NestCamera extends HomeKitDevice {
1183
1155
  // For a HKSV enabled camera, we will use this to trigger the starting of the HKSV recording if the camera is active
1184
1156
  if (event.types.includes('motion') === true) {
1185
1157
  if (this.motionTimer === undefined && (this.deviceData.hksv === false || this.streamer === undefined)) {
1186
- this?.log?.info && this.log.info('Motion detected at "%s"', deviceData.description);
1158
+ this?.log?.info?.('Motion detected at "%s"', deviceData.description);
1187
1159
  }
1188
1160
 
1189
1161
  event.zone_ids.forEach((zoneID) => {
@@ -1231,9 +1203,9 @@ export default class NestCamera extends HomeKitDevice {
1231
1203
  if (event.types.includes('person') === true || event.types.includes('face') === true) {
1232
1204
  if (this.personTimer === undefined) {
1233
1205
  // We don't have a person cooldown timer running, so we can process the 'person'/'face' event
1234
- if (this?.log?.info && (this.deviceData.hksv === false || this.streamer === undefined)) {
1206
+ if (this.deviceData.hksv === false || this.streamer === undefined) {
1235
1207
  // We'll only log a person detected event if HKSV is disabled
1236
- this.log.info('Person detected at "%s"', deviceData.description);
1208
+ this?.log?.info?.('Person detected at "%s"', deviceData.description);
1237
1209
  }
1238
1210
 
1239
1211
  // Cooldown for person being detected
@@ -1258,26 +1230,29 @@ export default class NestCamera extends HomeKitDevice {
1258
1230
  // This will help with any 'restored' service Homebridge has done
1259
1231
  // And allow for zone changes on the camera/doorbell
1260
1232
  this.motionServices = {};
1261
- this.accessory.services.forEach((service) => {
1262
- if (service.UUID === this.hap.Service.MotionSensor.UUID) {
1263
- this.accessory.removeService(service);
1264
- }
1265
- });
1233
+ this.accessory.services
1234
+ .filter((service) => service.UUID === this.hap.Service.MotionSensor.UUID)
1235
+ .forEach((service) => this.accessory.removeService(service));
1236
+
1237
+ let zones = Array.isArray(this.deviceData.activity_zones) === true ? this.deviceData.activity_zones : [];
1266
1238
 
1267
- if (this.deviceData.has_motion_detection === true && typeof this.deviceData.activity_zones === 'object') {
1239
+ if (this.deviceData.has_motion_detection === true && zones.length > 0) {
1268
1240
  // We have the capability of motion sensing on device, so setup motion sensor(s)
1269
1241
  // If we have HKSV video enabled, we'll only create a single motion sensor
1270
1242
  // A zone with the ID of 1 is treated as the main motion sensor
1271
- this.deviceData.activity_zones.forEach((zone) => {
1272
- if (this.deviceData.hksv === false || (this.deviceData.hksv === true && zone.id === 1)) {
1273
- let tempService = this.accessory.addService(this.hap.Service.MotionSensor, zone.id === 1 ? '' : zone.name, zone.id);
1274
- if (tempService.testCharacteristic(this.hap.Characteristic.Active) === false) {
1275
- tempService.addCharacteristic(this.hap.Characteristic.Active);
1276
- }
1277
- tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
1278
- this.motionServices[zone.id] = { service: tempService, timer: undefined };
1243
+ for (let zone of zones) {
1244
+ if (this.deviceData.hksv === true && zone.id !== 1) {
1245
+ continue;
1279
1246
  }
1280
- });
1247
+
1248
+ let zoneName = zone.id === 1 ? '' : zone.name;
1249
+ let service = this.addHKService(this.hap.Service.MotionSensor, zoneName, zone.id);
1250
+ this.addHKCharacteristic(service, this.hap.Characteristic.Active);
1251
+ service.updateCharacteristic(this.hap.Characteristic.Name, zoneName);
1252
+ service.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
1253
+
1254
+ this.motionServices[zone.id] = { service, timer: undefined };
1255
+ }
1281
1256
  }
1282
1257
  }
1283
1258