homebridge-nest-accfactory 0.2.11 → 0.3.0

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 +24 -0
  2. package/README.md +14 -7
  3. package/config.schema.json +118 -0
  4. package/dist/HomeKitDevice.js +194 -77
  5. package/dist/HomeKitHistory.js +1 -1
  6. package/dist/config.js +207 -0
  7. package/dist/devices.js +113 -0
  8. package/dist/index.js +2 -1
  9. package/dist/nexustalk.js +19 -21
  10. package/dist/{camera.js → plugins/camera.js} +212 -239
  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} +24 -41
  15. package/dist/{tempsensor.js → plugins/tempsensor.js} +13 -17
  16. package/dist/{thermostat.js → plugins/thermostat.js} +424 -381
  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 +68 -72
  40. package/dist/system.js +1208 -1245
  41. package/dist/webrtc.js +28 -23
  42. package/package.json +12 -12
  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,26 @@ 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 CAMERARESOURCE = {
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
30
  const SNAPSHOTCACHETIMEOUT = 30000; // Timeout for retaining snapshot image (in milliseconds)
31
+ const PROTOCOLWEBRTC = 'PROTOCOL_WEBRTC';
32
+ const PROTOCOLNEXUSTALK = 'PROTOCOL_NEXUSTALK';
33
+ const RESOURCEPATH = '../res';
30
34
  const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
31
35
 
32
36
  export default class NestCamera extends HomeKitDevice {
37
+ static TYPE = 'Camera';
38
+ static VERSION = '2025.06.12';
39
+
33
40
  controller = undefined; // HomeKit Camera/Doorbell controller service
34
41
  streamer = undefined; // Streamer object for live/recording stream
35
42
  motionServices = undefined; // Object of Camera/Doorbell motion sensor(s)
@@ -51,27 +58,25 @@ export default class NestCamera extends HomeKitDevice {
51
58
  constructor(accessory, api, log, eventEmitter, deviceData) {
52
59
  super(accessory, api, log, eventEmitter, deviceData);
53
60
 
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
- }
61
+ // Load supporrt image files as required
62
+ const loadImageIfExists = (filename, label) => {
63
+ let buffer = undefined;
64
+ let file = path.resolve(__dirname, RESOURCEPATH, filename);
65
+ if (fs.existsSync(file) === true) {
66
+ buffer = fs.readFileSync(file);
67
+ } else {
68
+ this.log?.warn?.('Failed to load %s image resource for "%s"', label, this.deviceData.description);
69
+ }
70
+ return buffer;
71
+ };
65
72
 
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
- }
73
+ this.#cameraOfflineImage = loadImageIfExists(CAMERARESOURCE.offline, 'offline');
74
+ this.#cameraVideoOffImage = loadImageIfExists(CAMERARESOURCE.off, 'video off');
75
+ this.#cameraTransferringImage = loadImageIfExists(CAMERARESOURCE.transfer, 'transferring');
71
76
  }
72
77
 
73
78
  // Class functions
74
- addServices(hapController = this.hap.CameraController) {
79
+ setupDevice(hapController = this.hap.CameraController) {
75
80
  // Setup motion services
76
81
  if (this.motionServices === undefined) {
77
82
  this.createCameraMotionServices();
@@ -92,19 +97,13 @@ export default class NestCamera extends HomeKitDevice {
92
97
  if (this.operatingModeService === undefined) {
93
98
  // Add in operating mode service for a non-hksv camera/doorbell
94
99
  // 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
- }
100
+ this.operatingModeService = this.addHKService(this.hap.Service.CameraOperatingMode, '', 1);
99
101
  }
100
102
 
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) => {
103
+ // Setup set characteristics
104
+ if (this.deviceData?.has_statusled === true) {
105
+ this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.CameraOperatingModeIndicator, {
106
+ onSet: (value) => {
108
107
  // 0 = auto, 1 = low, 2 = high
109
108
  // We'll use auto mode for led on and low for led off
110
109
  if (
@@ -112,43 +111,33 @@ export default class NestCamera extends HomeKitDevice {
112
111
  (value === false && this.deviceData.statusled_brightness !== 1)
113
112
  ) {
114
113
  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
- }
114
+ this?.log?.info?.('Recording status LED on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
118
115
  }
119
- });
120
-
121
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.CameraOperatingModeIndicator).onGet(() => {
116
+ },
117
+ onGet: () => {
122
118
  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
- }
119
+ },
120
+ });
121
+ }
130
122
 
131
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.NightVision).onSet((value) => {
123
+ if (this.deviceData?.has_irled === true) {
124
+ this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.NightVision, {
125
+ onSet: (value) => {
132
126
  // only change IRLed status value if different than on-device
133
127
  if ((value === false && this.deviceData.irled_enabled === true) || (value === true && this.deviceData.irled_enabled === false)) {
134
128
  this.set({ uuid: this.deviceData.nest_google_uuid, irled_enabled: value === true ? 'auto_on' : 'always_off' });
135
129
 
136
- if (this?.log?.info) {
137
- this.log.info('Night vision on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
138
- }
130
+ this?.log?.info?.('Night vision on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
139
131
  }
140
- });
141
-
142
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.NightVision).onGet(() => {
132
+ },
133
+ onGet: () => {
143
134
  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
- }
135
+ },
136
+ });
137
+ }
150
138
 
151
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.ManuallyDisabled).onSet((value) => {
139
+ this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.ManuallyDisabled, {
140
+ onSet: (value) => {
152
141
  if (value !== this.operatingModeService.getCharacteristic(this.hap.Characteristic.ManuallyDisabled).value) {
153
142
  // Make sure only updating status if HomeKit value *actually changes*
154
143
  if (
@@ -157,35 +146,31 @@ export default class NestCamera extends HomeKitDevice {
157
146
  ) {
158
147
  // Camera state does not reflect requested state, so fix
159
148
  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
- }
149
+ this?.log?.info?.('Camera on "%s" was turned', this.deviceData.description, value === false ? 'on' : 'off');
163
150
  }
164
151
  }
165
- });
166
-
167
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.ManuallyDisabled).onGet(() => {
152
+ },
153
+ onGet: () => {
168
154
  return this.deviceData.streaming_enabled === false
169
155
  ? this.hap.Characteristic.ManuallyDisabled.DISABLED
170
156
  : 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
- }
157
+ },
158
+ });
177
159
 
178
- this.operatingModeService.getCharacteristic(this.hap.Characteristic.ImageRotation).onGet(() => {
160
+ if (this.deviceData?.has_video_flip === true) {
161
+ this.addHKCharacteristic(this.operatingModeService, this.hap.Characteristic.ImageRotation, {
162
+ onGet: () => {
179
163
  return this.deviceData.video_flipped === true ? 180 : 0;
180
- });
181
- }
164
+ },
165
+ });
182
166
  }
183
167
 
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) => {
168
+ if (this.controller?.recordingManagement?.recordingManagementService !== undefined && this.deviceData.has_microphone === true) {
169
+ this.addHKCharacteristic(
170
+ this.controller.recordingManagement.recordingManagementService,
171
+ this.hap.Characteristic.RecordingAudioActive,
172
+ {
173
+ onSet: (value) => {
189
174
  if (
190
175
  (this.deviceData.audio_enabled === true && value === this.hap.Characteristic.RecordingAudioActive.DISABLE) ||
191
176
  (this.deviceData.audio_enabled === false && value === this.hap.Characteristic.RecordingAudioActive.ENABLE)
@@ -194,42 +179,37 @@ export default class NestCamera extends HomeKitDevice {
194
179
  uuid: this.deviceData.nest_google_uuid,
195
180
  audio_enabled: value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? true : false,
196
181
  });
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
- }
182
+ this?.log?.info?.(
183
+ 'Audio recording on "%s" was turned',
184
+ this.deviceData.description,
185
+ value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? 'on' : 'off',
186
+ );
204
187
  }
205
- });
206
-
207
- this.controller.recordingManagement.recordingManagementService
208
- .getCharacteristic(this.hap.Characteristic.RecordingAudioActive)
209
- .onGet(() => {
188
+ },
189
+ onGet: () => {
210
190
  return this.deviceData.audio_enabled === true
211
191
  ? this.hap.Characteristic.RecordingAudioActive.ENABLE
212
192
  : this.hap.Characteristic.RecordingAudioActive.DISABLE;
213
- });
214
- }
193
+ },
194
+ },
195
+ );
215
196
  }
216
197
 
217
198
  if (this.deviceData.migrating === true) {
218
199
  // 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);
200
+ this?.log?.warn?.('Migration between Nest <-> Google Home apps is underway for "%s"', this.deviceData.description);
220
201
  }
221
202
 
222
203
  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)
204
+ (this.deviceData.streaming_protocols.includes(PROTOCOLWEBRTC) === false &&
205
+ this.deviceData.streaming_protocols.includes(PROTOCOLNEXUSTALK) === false) ||
206
+ (this.deviceData.streaming_protocols.includes(PROTOCOLWEBRTC) === true && WebRTC === undefined) ||
207
+ (this.deviceData.streaming_protocols.includes(PROTOCOLNEXUSTALK) === true && NexusTalk === undefined)
227
208
  ) {
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
- );
209
+ this?.log?.error?.(
210
+ 'No suitable streaming protocol is present for "%s". Streaming and recording will be unavailable',
211
+ this.deviceData.description,
212
+ );
233
213
  }
234
214
 
235
215
  // Setup linkage to EveHome app if configured todo so
@@ -243,17 +223,13 @@ export default class NestCamera extends HomeKitDevice {
243
223
  });
244
224
  }
245
225
 
246
- // Create extra details for output
247
- let postSetupDetails = [];
226
+ // Extra setup details for output
248
227
  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;
228
+ this.postSetupDetail('HomeKit Secure Video support' + (this.streamer?.isBuffering() === true ? ' and recording buffer started' : ''));
229
+ this.deviceData.localAccess === true && this.postSetupDetail('Local access');
254
230
  }
255
231
 
256
- removeServices() {
232
+ removeDevice() {
257
233
  // Clean up our camera object since this device is being removed
258
234
  clearTimeout(this.motionTimer);
259
235
  clearTimeout(this.personTimer);
@@ -298,11 +274,10 @@ export default class NestCamera extends HomeKitDevice {
298
274
  // https://github.com/hjdhjd/homebridge-unifi-protect/blob/eee6a4e379272b659baa6c19986d51f5bf2cbbbc/src/protect-ffmpeg-record.ts
299
275
  async *handleRecordingStreamRequest(sessionID) {
300
276
  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
- );
277
+ this?.log?.warn?.(
278
+ 'Received request to start recording for "%s" however we do not have an ffmpeg binary present',
279
+ this.deviceData.description,
280
+ );
306
281
  return;
307
282
  }
308
283
 
@@ -312,16 +287,15 @@ export default class NestCamera extends HomeKitDevice {
312
287
  ) {
313
288
  // Should only be recording if motion detected.
314
289
  // 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');
290
+ this?.log?.debug?.('Received request to commence recording for "%s" however we have not detected any motion');
316
291
  return;
317
292
  }
318
293
 
319
294
  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
- );
295
+ this?.log?.error?.(
296
+ 'Received request to start recording for "%s" however we do not any associated streaming protocol support',
297
+ this.deviceData.description,
298
+ );
325
299
  return;
326
300
  }
327
301
 
@@ -410,12 +384,11 @@ export default class NestCamera extends HomeKitDevice {
410
384
  // Start our ffmpeg recording process and stream from our streamer
411
385
  // video is pipe #1
412
386
  // 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
- );
387
+ this?.log?.debug?.(
388
+ 'ffmpeg process for recording stream from "%s" will be called using the following commandline',
389
+ this.deviceData.description,
390
+ commandLine.join(' ').toString(),
391
+ );
419
392
  let ffmpegRecording = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
420
393
  env: process.env,
421
394
  stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
@@ -455,8 +428,7 @@ export default class NestCamera extends HomeKitDevice {
455
428
 
456
429
  ffmpegRecording.on('exit', (code, signal) => {
457
430
  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);
431
+ this?.log?.error?.('ffmpeg recording process for "%s" stopped unexpectedly. Exit code was "%s"', this.deviceData.description, code);
460
432
  }
461
433
 
462
434
  if (this.#hkSessions?.[sessionID] !== undefined) {
@@ -473,7 +445,7 @@ export default class NestCamera extends HomeKitDevice {
473
445
  ffmpegRecording.stderr.on('data', (data) => {
474
446
  if (data.toString().includes('frame=') === false && this.deviceData?.ffmpeg?.debug === true) {
475
447
  // Monitor ffmpeg output
476
- this?.log?.debug && this.log.debug(data.toString());
448
+ this?.log?.debug?.(data.toString());
477
449
  }
478
450
  });
479
451
 
@@ -486,8 +458,7 @@ export default class NestCamera extends HomeKitDevice {
486
458
  this.#hkSessions[sessionID].eventEmitter = eventEmitter;
487
459
  this.#hkSessions[sessionID].ffmpeg = ffmpegRecording; // Store ffmpeg process ID
488
460
 
489
- this?.log?.info &&
490
- this.log.info('Started recording from "%s" %s', this.deviceData.description, includeAudio === false ? 'without audio' : '');
461
+ this?.log?.info?.('Started recording from "%s" %s', this.deviceData.description, includeAudio === false ? 'without audio' : '');
491
462
 
492
463
  // Loop generating MOOF/MDAT box pairs for HomeKit Secure Video.
493
464
  // HAP-NodeJS cancels this async generator function when recording completes also
@@ -539,14 +510,13 @@ export default class NestCamera extends HomeKitDevice {
539
510
 
540
511
  // Log recording finished messages depending on reason
541
512
  if (closeReason === this.hap.HDSProtocolSpecificErrorReason.NORMAL) {
542
- this?.log?.info && this.log.info('Completed recording from "%s"', this.deviceData.description);
513
+ this?.log?.info?.('Completed recording from "%s"', this.deviceData.description);
543
514
  } 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
- );
515
+ this?.log?.warn?.(
516
+ 'Recording from "%s" completed with error. Reason was "%s"',
517
+ this.deviceData.description,
518
+ this.hap.HDSProtocolSpecificErrorReason[closeReason],
519
+ );
550
520
  }
551
521
  }
552
522
 
@@ -555,13 +525,13 @@ export default class NestCamera extends HomeKitDevice {
555
525
  // Start a buffering stream for this camera/doorbell. Ensures motion captures all video on motion trigger
556
526
  // Required due to data delays by on prem Nest to cloud to HomeKit accessory to iCloud etc
557
527
  // Make sure have appropriate bandwidth!!!
558
- this?.log?.info && this.log.info('Recording was turned on for "%s"', this.deviceData.description);
528
+ this?.log?.info?.('Recording was turned on for "%s"', this.deviceData.description);
559
529
  this.streamer.startBuffering();
560
530
  }
561
531
 
562
532
  if (enableRecording === false && this.streamer?.isBuffering() === true) {
563
533
  this.streamer.stopBuffering();
564
- this?.log?.warn && this.log.warn('Recording was turned off for "%s"', this.deviceData.description);
534
+ this?.log?.warn?.('Recording was turned off for "%s"', this.deviceData.description);
565
535
  }
566
536
  }
567
537
 
@@ -688,20 +658,18 @@ export default class NestCamera extends HomeKitDevice {
688
658
  // called when HomeKit asks to start/stop/reconfigure a camera/doorbell live stream
689
659
  if (request.type === this.hap.StreamRequestTypes.START && this.streamer === undefined) {
690
660
  // 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
- );
661
+ this?.log?.error?.(
662
+ 'Received request to start live video for "%s" however we do not any associated streaming protocol support',
663
+ this.deviceData.description,
664
+ );
696
665
  }
697
666
 
698
667
  if (request.type === this.hap.StreamRequestTypes.START && this.deviceData?.ffmpeg?.binary === undefined) {
699
668
  // 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
- );
669
+ this?.log?.warn?.(
670
+ 'Received request to start live video for "%s" however we do not have an ffmpeg binary present',
671
+ this.deviceData.description,
672
+ );
705
673
  }
706
674
 
707
675
  if (
@@ -802,12 +770,11 @@ export default class NestCamera extends HomeKitDevice {
802
770
  // Start our ffmpeg streaming process and stream from our streamer
803
771
  // video is pipe #1
804
772
  // 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
- );
773
+ this?.log?.debug?.(
774
+ 'ffmpeg process for live streaming from "%s" will be called using the following commandline',
775
+ this.deviceData.description,
776
+ commandLine.join(' ').toString(),
777
+ );
811
778
  let ffmpegStreaming = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
812
779
  env: process.env,
813
780
  stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
@@ -815,12 +782,11 @@ export default class NestCamera extends HomeKitDevice {
815
782
 
816
783
  ffmpegStreaming.on('exit', (code, signal) => {
817
784
  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
- );
785
+ this?.log?.error?.(
786
+ 'ffmpeg video/audio live streaming process for "%s" stopped unexpectedly. Exit code was "%s"',
787
+ this.deviceData.description,
788
+ code,
789
+ );
824
790
 
825
791
  // Clean up or streaming request, but calling it again with a 'STOP' reques
826
792
  this.handleStreamRequest({ type: this.hap.StreamRequestTypes.STOP, sessionID: request.sessionID }, null);
@@ -831,7 +797,7 @@ export default class NestCamera extends HomeKitDevice {
831
797
  ffmpegStreaming.stderr.on('data', (data) => {
832
798
  if (data.toString().includes('frame=') === false && this.deviceData?.ffmpeg?.debug === true) {
833
799
  // Monitor ffmpeg output
834
- this?.log?.debug && this.log.debug(data.toString());
800
+ this?.log?.debug?.(data.toString());
835
801
  }
836
802
  });
837
803
 
@@ -894,24 +860,22 @@ export default class NestCamera extends HomeKitDevice {
894
860
 
895
861
  commandLine.push('-f data pipe:1');
896
862
 
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
- );
863
+ this?.log?.debug?.(
864
+ 'ffmpeg process for talkback on "%s" will be called using the following commandline',
865
+ this.deviceData.description,
866
+ commandLine.join(' ').toString(),
867
+ );
903
868
  ffmpegAudioTalkback = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
904
869
  env: process.env,
905
870
  });
906
871
 
907
872
  ffmpegAudioTalkback.on('exit', (code, signal) => {
908
873
  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
- );
874
+ this?.log?.error?.(
875
+ 'ffmpeg audio talkback streaming process for "%s" stopped unexpectedly. Exit code was "%s"',
876
+ this.deviceData.description,
877
+ code,
878
+ );
915
879
 
916
880
  // Clean up or streaming request, but calling it again with a 'STOP' request
917
881
  this.handleStreamRequest({ type: this.hap.StreamRequestTypes.STOP, sessionID: request.sessionID }, null);
@@ -927,7 +891,7 @@ export default class NestCamera extends HomeKitDevice {
927
891
  ffmpegAudioTalkback.stderr.on('data', (data) => {
928
892
  if (data.toString().includes('frame=') === false && this.deviceData?.ffmpeg?.debug === true) {
929
893
  // Monitor ffmpeg output
930
- this?.log?.debug && this.log.debug(data.toString());
894
+ this?.log?.debug?.(data.toString());
931
895
  }
932
896
  });
933
897
 
@@ -971,12 +935,11 @@ export default class NestCamera extends HomeKitDevice {
971
935
  ffmpegAudioTalkback.stdin.end();
972
936
  }
973
937
 
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
- );
938
+ this?.log?.info?.(
939
+ 'Live stream started on "%s" %s',
940
+ this.deviceData.description,
941
+ ffmpegAudioTalkback?.stdout ? 'with two-way audio' : '',
942
+ );
980
943
 
981
944
  // Start the appropriate streamer
982
945
  this.streamer !== undefined &&
@@ -1010,11 +973,11 @@ export default class NestCamera extends HomeKitDevice {
1010
973
 
1011
974
  delete this.#hkSessions[request.sessionID];
1012
975
 
1013
- this?.log?.info && this.log.info('Live stream stopped from "%s"', this.deviceData.description);
976
+ this?.log?.info?.('Live stream stopped from "%s"', this.deviceData.description);
1014
977
  }
1015
978
 
1016
979
  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);
980
+ this?.log?.debug?.('Unsupported reconfiguration request for live stream on "%s"', this.deviceData.description);
1018
981
  }
1019
982
 
1020
983
  if (typeof callback === 'function') {
@@ -1022,32 +985,32 @@ export default class NestCamera extends HomeKitDevice {
1022
985
  }
1023
986
  }
1024
987
 
1025
- updateServices(deviceData) {
988
+ updateDevice(deviceData) {
1026
989
  if (typeof deviceData !== 'object' || this.controller === undefined) {
1027
990
  return;
1028
991
  }
1029
992
 
1030
993
  if (this.deviceData.migrating === false && deviceData.migrating === true) {
1031
994
  // 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);
995
+ this?.log?.warn?.('Migration between Nest <-> Google Home apps has started for "%s"', deviceData.description);
1033
996
  this.streamer !== undefined && this.streamer.stopEverything();
1034
997
  this.streamer = undefined;
1035
998
  }
1036
999
 
1037
1000
  if (this.deviceData.migrating === true && deviceData.migrating === false) {
1038
1001
  // 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);
1002
+ this?.log?.success?.('Migration between Nest <-> Google Home apps has completed for "%s"', deviceData.description);
1040
1003
  }
1041
1004
 
1042
1005
  // Handle case of changes in streaming protocols OR just finished migration between Nest <-> Google Home apps
1043
1006
  if (this.streamer === undefined && deviceData.migrating === false) {
1044
1007
  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);
1008
+ this?.log?.warn?.('Available streaming protocols have changed for "%s"', deviceData.description);
1046
1009
  this.streamer !== undefined && this.streamer.stopEverything();
1047
1010
  this.streamer = undefined;
1048
1011
  }
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);
1012
+ if (deviceData.streaming_protocols.includes(PROTOCOLWEBRTC) === true && WebRTC !== undefined) {
1013
+ this?.log?.debug?.('Using WebRTC streamer for "%s"', deviceData.description);
1051
1014
  this.streamer = new WebRTC(deviceData, {
1052
1015
  log: this.log,
1053
1016
  buffer:
@@ -1058,8 +1021,8 @@ export default class NestCamera extends HomeKitDevice {
1058
1021
  });
1059
1022
  }
1060
1023
 
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);
1024
+ if (deviceData.streaming_protocols.includes(PROTOCOLNEXUSTALK) === true && NexusTalk !== undefined) {
1025
+ this?.log?.debug?.('Using NexusTalk streamer for "%s"', deviceData.description);
1063
1026
  this.streamer = new NexusTalk(deviceData, {
1064
1027
  log: this.log,
1065
1028
  buffer:
@@ -1072,22 +1035,26 @@ export default class NestCamera extends HomeKitDevice {
1072
1035
  }
1073
1036
 
1074
1037
  // 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);
1038
+ if (
1039
+ Array.isArray(deviceData.activity_zones) === true &&
1040
+ JSON.stringify(deviceData.activity_zones) !== JSON.stringify(this.deviceData.activity_zones)
1041
+ ) {
1042
+ deviceData.activity_zones.forEach((zone) => {
1043
+ if (this.deviceData.hksv === false || (this.deviceData.hksv === true && zone.id === 1)) {
1044
+ if (this.motionServices?.[zone.id]?.service === undefined) {
1045
+ // Zone doesn't have an associated motion sensor, so add one
1046
+ let zoneName = zone.id === 1 ? '' : zone.name;
1047
+ let tempService = this.addHKService(this.hap.Service.MotionSensor, zoneName, zone.id);
1048
+
1049
+ this.addHKCharacteristic(tempService, this.hap.Characteristic.Active);
1050
+ tempService.updateCharacteristic(this.hap.Characteristic.Name, zoneName);
1051
+ tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
1052
+
1053
+ this.motionServices[zone.id] = { service: tempService, timer: undefined };
1085
1054
  }
1086
- tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
1087
- this.motionServices[zone.id] = { service: tempService, timer: undefined };
1088
1055
  }
1089
- }
1090
- });
1056
+ });
1057
+ }
1091
1058
 
1092
1059
  // Check to see if any activity zones were removed for both non-HKSV and HKSV enabled devices
1093
1060
  // We'll also update the online status of the camera in the motion service here
@@ -1098,12 +1065,15 @@ export default class NestCamera extends HomeKitDevice {
1098
1065
  deviceData.online === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
1099
1066
  );
1100
1067
 
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
- }
1068
+ // Handle deleted zones (excluding zone ID 1 for HKSV)
1069
+ if (
1070
+ zoneID !== '1' &&
1071
+ Array.isArray(deviceData.activity_zones) === true &&
1072
+ deviceData.activity_zones.findIndex(({ id }) => id === Number(zoneID)) === -1
1073
+ ) {
1074
+ // Motion service we created doesn't appear in zone list anymore, so assume deleted
1075
+ this.accessory.removeService(service.service);
1076
+ delete this.motionServices[zoneID];
1107
1077
  }
1108
1078
  });
1109
1079
 
@@ -1116,7 +1086,7 @@ export default class NestCamera extends HomeKitDevice {
1116
1086
  : this.hap.Characteristic.ManuallyDisabled.ENABLED,
1117
1087
  );
1118
1088
 
1119
- if (deviceData.has_statusled === true) {
1089
+ if (deviceData?.has_statusled === true) {
1120
1090
  // Set camera recording indicator. This cannot be turned off on Nest Cameras/Doorbells
1121
1091
  // 0 = auto
1122
1092
  // 1 = low
@@ -1127,12 +1097,12 @@ export default class NestCamera extends HomeKitDevice {
1127
1097
  );
1128
1098
  }
1129
1099
 
1130
- if (deviceData.has_irled === true) {
1100
+ if (deviceData?.has_irled === true) {
1131
1101
  // Set nightvision status in HomeKit
1132
1102
  this.operatingModeService.updateCharacteristic(this.hap.Characteristic.NightVision, deviceData.irled_enabled);
1133
1103
  }
1134
1104
 
1135
- if (deviceData.has_video_flip === true) {
1105
+ if (deviceData?.has_video_flip === true) {
1136
1106
  // Update image flip status
1137
1107
  this.operatingModeService.updateCharacteristic(this.hap.Characteristic.ImageRotation, deviceData.video_flipped === true ? 180 : 0);
1138
1108
  }
@@ -1183,7 +1153,7 @@ export default class NestCamera extends HomeKitDevice {
1183
1153
  // For a HKSV enabled camera, we will use this to trigger the starting of the HKSV recording if the camera is active
1184
1154
  if (event.types.includes('motion') === true) {
1185
1155
  if (this.motionTimer === undefined && (this.deviceData.hksv === false || this.streamer === undefined)) {
1186
- this?.log?.info && this.log.info('Motion detected at "%s"', deviceData.description);
1156
+ this?.log?.info?.('Motion detected at "%s"', deviceData.description);
1187
1157
  }
1188
1158
 
1189
1159
  event.zone_ids.forEach((zoneID) => {
@@ -1231,9 +1201,9 @@ export default class NestCamera extends HomeKitDevice {
1231
1201
  if (event.types.includes('person') === true || event.types.includes('face') === true) {
1232
1202
  if (this.personTimer === undefined) {
1233
1203
  // 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)) {
1204
+ if (this.deviceData.hksv === false || this.streamer === undefined) {
1235
1205
  // We'll only log a person detected event if HKSV is disabled
1236
- this.log.info('Person detected at "%s"', deviceData.description);
1206
+ this?.log?.info?.('Person detected at "%s"', deviceData.description);
1237
1207
  }
1238
1208
 
1239
1209
  // Cooldown for person being detected
@@ -1258,26 +1228,29 @@ export default class NestCamera extends HomeKitDevice {
1258
1228
  // This will help with any 'restored' service Homebridge has done
1259
1229
  // And allow for zone changes on the camera/doorbell
1260
1230
  this.motionServices = {};
1261
- this.accessory.services.forEach((service) => {
1262
- if (service.UUID === this.hap.Service.MotionSensor.UUID) {
1263
- this.accessory.removeService(service);
1264
- }
1265
- });
1231
+ this.accessory.services
1232
+ .filter((service) => service.UUID === this.hap.Service.MotionSensor.UUID)
1233
+ .forEach((service) => this.accessory.removeService(service));
1234
+
1235
+ let zones = Array.isArray(this.deviceData.activity_zones) === true ? this.deviceData.activity_zones : [];
1266
1236
 
1267
- if (this.deviceData.has_motion_detection === true && typeof this.deviceData.activity_zones === 'object') {
1237
+ if (this.deviceData.has_motion_detection === true && zones.length > 0) {
1268
1238
  // We have the capability of motion sensing on device, so setup motion sensor(s)
1269
1239
  // If we have HKSV video enabled, we'll only create a single motion sensor
1270
1240
  // 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 };
1241
+ for (let zone of zones) {
1242
+ if (this.deviceData.hksv === true && zone.id !== 1) {
1243
+ continue;
1279
1244
  }
1280
- });
1245
+
1246
+ let zoneName = zone.id === 1 ? '' : zone.name;
1247
+ let service = this.addHKService(this.hap.Service.MotionSensor, zoneName, zone.id);
1248
+ this.addHKCharacteristic(service, this.hap.Characteristic.Active);
1249
+ service.updateCharacteristic(this.hap.Characteristic.Name, zoneName);
1250
+ service.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
1251
+
1252
+ this.motionServices[zone.id] = { service, timer: undefined };
1253
+ }
1281
1254
  }
1282
1255
  }
1283
1256