homebridge-nest-accfactory 0.3.3 → 0.3.4

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `homebridge-nest-accfactory` will be documented in this file. This project tries to adhere to [Semantic Versioning](http://semver.org/).
4
4
 
5
+ ## v0.3.4 (2025/10/17)
6
+
7
+ - General code cleanup and stability improvements
8
+ - General typo and grammar corrections
9
+ - Fixed handling of the per-device `hksv` setting
10
+ - Fixed thermostat cooling stage 3 checking
11
+ - Fixed thermostat fan state checking
12
+ - Fixed periodic camera snapshots when camera is turned off
13
+ - Updated camera resource assets
14
+
5
15
  ## v0.3.3 (2025/08/23)
6
16
 
7
17
  - Refined timeout warnings for camera and doorbell snapshot capture
package/README.md CHANGED
@@ -48,7 +48,7 @@ There may be other keys labelled access_token further along in the string - plea
48
48
 
49
49
  ### Obtaining a Google cookie token for a Google Account
50
50
 
51
- Google Accounts require an "issueToken" and "cookie". The values of "issueToken" and "cookies" are specific to your Google Account. To get them, follow these steps (only needs to be done once, as long as you stay logged into your Google Account).
51
+ Google Accounts require an "issueToken" and "cookie". The values of "issueToken" and "cookie" are specific to your Google Account. To get them, follow these steps (only needs to be done once, as long as you stay logged into your Google Account).
52
52
 
53
53
  1. Open a Chrome browser tab in Incognito Mode
54
54
  2. Open Developer Tools (View/Developer/Developer Tools).
@@ -57,15 +57,15 @@ Google Accounts require an "issueToken" and "cookie". The values of "issueToken"
57
57
  5. Go to home.nest.com, and click 'Sign in with Google'. Log into your account.
58
58
  6. One network call (beginning with iframerpc) will appear in the Dev Tools window. Click on it.
59
59
  7. In the Headers tab, under General, copy the entire Request URL (beginning with https://accounts.google.com). This is your "Issue Token" which can be entered into the plugin-configuration within Homebridge.
60
- 9. In the 'Filter' box, enter oauth2/iframe
61
- 10. Several network calls will appear in the Dev Tools window. Click on the last iframe call.
62
- 11. In the Headers tab, under Request Headers, copy the entire cookie (include the whole string which is several lines long and has many field/value pairs - do not include the cookie: name). This is your "Cookie" which can be entered into the plugin-configuration within Homebridge.
60
+ 8. In the 'Filter' box, enter oauth2/iframe
61
+ 9. Several network calls will appear in the Dev Tools window. Click on the last iframe call.
62
+ 10. In the Headers tab, under Request Headers, copy the entire cookie (include the whole string which is several lines long and has many field/value pairs - do not include the cookie: name). This is your "Cookie" which can be entered into the plugin-configuration within Homebridge.
63
63
 
64
64
  **Do not log out of home.nest.com, as this will invalidate your credentials. Just close the browser tab**
65
65
 
66
66
  ## config.json configuration
67
67
 
68
- When using the plugin configuration using [homebridge-config-ui-x](https://github.com/homebridge/homebridge-config-ui-x), the config.json will be updated/generated with the configuration options available via the web-form. Additional options can be specified in the config.json directly.
68
+ When using the plugin configuration with [homebridge-config-ui-x](https://github.com/homebridge/homebridge-config-ui-x), the config.json will be updated/generated with the configuration options available via the web-form. Additional options can be specified in the config.json directly.
69
69
 
70
70
  Sample config.json entries below
71
71
  ```
@@ -90,8 +90,9 @@ Sample config.json entries below
90
90
  "serialNumber": "XXXXXXXX",
91
91
  "exclude": false
92
92
  },
93
+ {
93
94
  "serialNumber": "YYYYYYYY",
94
- "hksv" : true
95
+ "hksv": true
95
96
  }
96
97
  ],
97
98
  "platform": "NestAccfactory"
@@ -107,9 +108,9 @@ The following options are available in the config.json options object. These app
107
108
  | elevation | Height above sea level for the weather station | 0 |
108
109
  | eveHistory | Provide history in EveHome application where applicable | true |
109
110
  | exclude | Exclude ALL devices | false |
110
- | ffmpegDebug | Turns on specific debugging output for when ffmpeg is envoked | false |
111
+ | ffmpegDebug | Turns on specific debugging output for when ffmpeg is invoked | false |
111
112
  | ffmpegHWaccel | Enable video hardware acceleration for supported camera(s) and doorbell(s) | false |
112
- | ffmegPath | Path to an ffmpeg binary (looks for binary named `ffmpeg` in path) | /usr/local/bin |
113
+ | ffmpegPath | Path to an ffmpeg binary (looks for binary named `ffmpeg` in path) | /usr/local/bin |
113
114
  | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
114
115
  | weather | Virtual weather station for each Nest/Google home we discover | false |
115
116
 
@@ -120,22 +121,22 @@ The following options are available on a per-device level in the `config.json` `
120
121
  | Name | Description | Default |
121
122
  |--------------------|----------------------------------------------------------------------------------------------|----------------|
122
123
  | chimeSwitch | Create a switch for supported doorbell(s) which allows the indoor chime to be turned on/off | false |
123
- | doorbellCooldown | Time in seconds between doorbell press events | 60 |
124
+ | doorbellCooldown | Time in seconds between doorbell press events | 60 |
124
125
  | elevation | Height above sea level for the specific weather station | 0 |
125
126
  | eveHistory | Provide history in EveHome application where applicable for the specific device | true |
126
127
  | exclude | Exclude the device | false |
127
128
  | fanDuration | Override fan runtime duration | |
128
- | ffmpegDebug | Turns on specific debugging output for when ffmpeg is envoked | false |
129
+ | ffmpegDebug | Turns on specific debugging output for when ffmpeg is invoked | false |
129
130
  | ffmpegHWaccel | Enable video hardware acceleration for supported camera(s) and doorbell(s) | false |
130
131
  | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
131
132
  | hotwaterBoostTime | Time in seconds for hotwater boost heating (30, 160, 120mins) | 30mins |
132
133
  | hotwaterMaxTemp | Maximum supported temperature for hotwater heating | 70c |
133
134
  | hotwaterMinTemp | Minimum supported temperature for hotwater heating | 30c |
134
- | humiditySensor | Create a seperate humidity sensor for supported thermostat(s) | false |
135
- | localAccess | Use direct access to supported camera(s) and doorbell(s) for video streaming and recording | false |
135
+ | humiditySensor | Create a separate humidity sensor for supported thermostat(s) | false |
136
+ | localAccess | Use direct access to supported camera(s) and doorbell(s) for video streaming and recording | false |
136
137
  | motionCooldown | Time in seconds between detected motion events | 60 |
137
138
  | personCooldown | Time in seconds between detected person events | 120 |
138
- | serialNumber | Device serial number to which these settings belong too | |
139
+ | serialNumber | Device serial number to which these settings belong to | |
139
140
 
140
141
  ## ffmpeg
141
142
 
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Configuration validation and processing
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 2025.08.18
4
+ // Code version 2025.09.08
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
@@ -34,7 +34,7 @@ function processConfig(config, log) {
34
34
  // Get configuration for max number of concurrent 'live view' streams. For HomeKit Secure Video, this will always be 1
35
35
  options.maxStreams = isNaN(config.options?.maxStreams) === false ? Number(config.options.maxStreams) : 2;
36
36
 
37
- // Check if a ffmpeg binary exist via a specific path in configuration OR /usr/local/bin
37
+ // Check if an ffmpeg binary exists via a specific path in configuration OR /usr/local/bin
38
38
  options.ffmpeg = {
39
39
  binary: undefined,
40
40
  valid: false,
@@ -127,7 +127,9 @@ function processConfig(config, log) {
127
127
  log?.warn?.('');
128
128
  log?.warn?.('NOTICE');
129
129
  log?.warn?.('> The per device configuration contains legacy options. Please review the readme at the link below');
130
- log?.warn?.('> Consider updating your configuration file as the mapping from legacy to current per device configuration maybe removed');
130
+ log?.warn?.(
131
+ '> Consider updating your configuration file as the mapping from legacy to current per device configuration may be removed',
132
+ );
131
133
  log?.warn?.('> https://github.com/n0rt0nthec4t/homebridge-nest-accfactory/blob/main/src/README.md');
132
134
  log?.warn?.('');
133
135
  }
package/dist/consts.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Common defines
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 2025.08.20
4
+ // Code version 2025.09.08
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Homebridge platform allowing Nest devices to be used with HomeKit
2
- // This is a port from my standalone project, Nest_accfactory to Homebridge
2
+ // This is a port from my standalone project, NestAccfactory to Homebridge
3
3
  //
4
4
  // This includes having support for HomeKit Secure Video (HKSV) on doorbells and cameras
5
5
  //
@@ -17,7 +17,7 @@
17
17
  //
18
18
  // Supports both Nest REST and Protobuf APIs for communication
19
19
  //
20
- // Code version 2025.07.29
20
+ // Code version 2025.09.08
21
21
  // Mark Hulskamp
22
22
  'use strict';
23
23
 
package/dist/nexustalk.js CHANGED
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // Credit to https://github.com/Brandawg93/homebridge-nest-cam for the work on the Nest Camera comms code on which this is based
7
7
  //
8
- // Code version 2025.07.30
8
+ // Code version 2025.09.08
9
9
  // Mark Hulskamp
10
10
  'use strict';
11
11
 
@@ -98,7 +98,7 @@ export default class NexusTalk extends Streamer {
98
98
  this.#protobufNexusTalk = protobuf.loadSync(path.resolve(__dirname + '/protobuf/nest/nexustalk.proto'));
99
99
  }
100
100
 
101
- // Store data we need from the device data passed it
101
+ // Store data we need from the device data passed in
102
102
  this.token = deviceData?.apiAccess?.token;
103
103
  this.streaming_host = deviceData?.streaming_host; // Host we'll connect to
104
104
  this.useGoogleAuth = typeof deviceData?.apiAccess?.oauth2 === 'string' && deviceData?.apiAccess?.oauth2 !== '';
@@ -175,10 +175,10 @@ export default class NexusTalk extends Streamer {
175
175
  }
176
176
 
177
177
  async close(stopStreamFirst) {
178
- // Close an authenicated socket stream gracefully
178
+ // Close an authenticated socket stream gracefully
179
179
  if (this.#socket !== undefined) {
180
180
  if (stopStreamFirst === true) {
181
- // Send a notifcation to nexus we're finished playback
181
+ // Send a notification to nexus that we've finished playback
182
182
  await this.#stopNexusData();
183
183
  }
184
184
  this.#socket.destroy();
@@ -288,7 +288,7 @@ export default class NexusTalk extends Streamer {
288
288
 
289
289
  #sendMessage(type, data) {
290
290
  if (this.#socket?.readyState !== 'open' || (type !== PACKET_TYPE.HELLO && this.#authorised === false)) {
291
- // We're not connect and/or authorised yet, so 'cache' message for processing once this occurs
291
+ // We're not connected and/or authorised yet, so 'cache' message for processing once this occurs
292
292
  this.#messages.push({ type: type, data: data });
293
293
  return;
294
294
  }
@@ -314,7 +314,7 @@ export default class NexusTalk extends Streamer {
314
314
  #Authenticate(reauthorise) {
315
315
  // Authenticate over created socket connection
316
316
  if (this.#protobufNexusTalk !== undefined) {
317
- this.#authorised = false; // We're nolonger authorised
317
+ this.#authorised = false; // We're no longer authorised
318
318
 
319
319
  let authoriseRequest = null;
320
320
  let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.AuthoriseRequest');
@@ -429,8 +429,8 @@ export default class NexusTalk extends Streamer {
429
429
  if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
430
430
  let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.PlaybackPacket').decode(payload).toJSON();
431
431
 
432
- // Setup up a timeout to monitor for no packets recieved in a certain period
433
- // If its trigger, we'll attempt to restart the stream and/or connection
432
+ // Set up a timeout to monitor for no packets received in a certain period
433
+ // If it's triggered, we'll attempt to restart the stream and/or connection
434
434
  // <-- testing to see how often this occurs first
435
435
  clearTimeout(this.#stalledTimer);
436
436
  this.#stalledTimer = setTimeout(() => {
@@ -464,7 +464,7 @@ export default class NexusTalk extends Streamer {
464
464
  }
465
465
 
466
466
  #handlePlaybackEnd(payload) {
467
- // Decode playpack ended packet
467
+ // Decode playback ended packet
468
468
  if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
469
469
  let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.PlaybackEnd').decode(payload).toJSON();
470
470
 
@@ -519,7 +519,7 @@ export default class NexusTalk extends Streamer {
519
519
  }
520
520
 
521
521
  #handleNexusData(data) {
522
- // Process the rawdata from our socket connection and convert into nexus packets to take action against
522
+ // Process the raw data from our socket connection and convert into nexus packets to take action against
523
523
  this.#packets = this.#packets.length === 0 ? data : Buffer.concat([this.#packets, data]);
524
524
 
525
525
  while (this.#packets.length >= 3) {
@@ -533,7 +533,7 @@ export default class NexusTalk extends Streamer {
533
533
  }
534
534
 
535
535
  if (this.#packets.length < headerSize + packetSize) {
536
- // We dont have enough data in the buffer yet to process the full packet
536
+ // We don't have enough data in the buffer yet to process the full packet
537
537
  // so, exit loop and await more data
538
538
  break;
539
539
  }
@@ -31,7 +31,7 @@ const STREAMING_PROTOCOL = {
31
31
 
32
32
  export default class NestCamera extends HomeKitDevice {
33
33
  static TYPE = 'Camera';
34
- static VERSION = '2025.08.19'; // Code version
34
+ static VERSION = '2025.09.08'; // Code version
35
35
 
36
36
  // For messaging back to parent class (Doorbell/Floodlight)
37
37
  static SET = HomeKitDevice.SET;
@@ -84,7 +84,7 @@ export default class NestCamera extends HomeKitDevice {
84
84
  onAdd() {
85
85
  // Handle HKSV configuration change from enabled/disable states
86
86
  if (typeof this?.accessory?.context?.hksv === 'boolean' && this.accessory.context.hksv !== this.deviceData.hksv) {
87
- // We need to remove the CameraOperaionMode service to avoid errors when setting up the HomeKit controller
87
+ // We need to remove the CameraOperatingMode service to avoid errors when setting up the HomeKit controller
88
88
  // Thanks to @bcullman (Brad Ullman) for catching this
89
89
  if (this.accessory.getService(this.hap.Service.CameraOperatingMode) !== undefined) {
90
90
  this.accessory.removeService(this.accessory.getService(this.hap.Service.CameraOperatingMode));
@@ -103,7 +103,7 @@ export default class NestCamera extends HomeKitDevice {
103
103
  this.controller = new this.hap.CameraController(this.generateControllerOptions());
104
104
  }
105
105
  if (this.controller !== undefined) {
106
- // Configure the controller thats been created
106
+ // Configure the controller that's been created
107
107
  this.accessory.configureController(this.controller);
108
108
  }
109
109
 
@@ -130,7 +130,7 @@ export default class NestCamera extends HomeKitDevice {
130
130
  (value === false && this.deviceData.statusled_brightness !== 1)
131
131
  ) {
132
132
  this.message(HomeKitDevice.SET, { uuid: this.deviceData.nest_google_uuid, statusled_brightness: value === true ? 0 : 1 });
133
- this?.log?.info?.('Recording status LED on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
133
+ this?.log?.info?.('Recording status LED on "%s" was turned %s', this.deviceData.description, value === true ? 'on' : 'off');
134
134
  }
135
135
  },
136
136
  onGet: () => {
@@ -150,7 +150,7 @@ export default class NestCamera extends HomeKitDevice {
150
150
  irled_enabled: value === true ? 'auto_on' : 'always_off',
151
151
  });
152
152
 
153
- this?.log?.info?.('Night vision on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
153
+ this?.log?.info?.('Night vision on "%s" was turned %s', this.deviceData.description, value === true ? 'on' : 'off');
154
154
  }
155
155
  },
156
156
  onGet: () => {
@@ -176,18 +176,13 @@ export default class NestCamera extends HomeKitDevice {
176
176
  streaming_enabled: value === this.hap.Characteristic.HomeKitCameraActive.ON ? true : false,
177
177
  });
178
178
  this?.log?.info?.(
179
- 'Camera on "%s" was turned',
179
+ 'Camera on "%s" was turned %s',
180
180
  this.deviceData.description,
181
181
  value === this.hap.Characteristic.HomeKitCameraActive.ON ? 'on' : 'off',
182
182
  );
183
183
  }
184
184
  }
185
185
  },
186
- onGet: () => {
187
- return this.deviceData.streaming_enabled === false
188
- ? this.hap.Characteristic.HomeKitCameraActive.OFF
189
- : this.hap.Characteristic.HomeKitCameraActive.ON;
190
- },
191
186
  });
192
187
 
193
188
  if (this.deviceData?.has_video_flip === true) {
@@ -213,7 +208,7 @@ export default class NestCamera extends HomeKitDevice {
213
208
  audio_enabled: value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? true : false,
214
209
  });
215
210
  this?.log?.info?.(
216
- 'Audio recording on "%s" was turned',
211
+ 'Audio recording on "%s" was turned %s',
217
212
  this.deviceData.description,
218
213
  value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? 'on' : 'off',
219
214
  );
@@ -397,10 +392,10 @@ export default class NestCamera extends HomeKitDevice {
397
392
  if (this.controller?.recordingManagement?.operatingModeService !== undefined) {
398
393
  // Update camera off/on status
399
394
  this.controller.recordingManagement.operatingModeService.updateCharacteristic(
400
- this.hap.Characteristic.HomeKitCameraActive,
395
+ this.hap.Characteristic.ManuallyDisabled,
401
396
  deviceData.streaming_enabled === true
402
- ? this.hap.Characteristic.HomeKitCameraActive.ON
403
- : this.hap.Characteristic.HomeKitCameraActive.OFF,
397
+ ? this.hap.Characteristic.ManuallyDisabled.ENABLED
398
+ : this.hap.Characteristic.ManuallyDisabled.DISABLED,
404
399
  );
405
400
 
406
401
  if (deviceData?.has_statusled === true) {
@@ -431,19 +426,15 @@ export default class NestCamera extends HomeKitDevice {
431
426
  }
432
427
 
433
428
  if (this.deviceData.hksv === false) {
434
- // Update snapshot status for non-hksv in operating mode service
429
+ // Specific settings for non-HKSV camera's
435
430
  this.controller.recordingManagement.operatingModeService.updateCharacteristic(
436
- this.hap.Characteristic.EventSnapshotsActive,
437
- deviceData.streaming_enabled === true
438
- ? this.hap.Characteristic.EventSnapshotsActive.ENABLE
439
- : this.hap.Characteristic.EventSnapshotsActive.DISABLE,
431
+ this.hap.Characteristic.PeriodicSnapshotsActive,
432
+ this.hap.Characteristic.PeriodicSnapshotsActive.ENABLE,
440
433
  );
441
434
 
442
435
  this.controller.recordingManagement.operatingModeService.updateCharacteristic(
443
- this.hap.Characteristic.PeriodicSnapshotsActive,
444
- deviceData.streaming_enabled === true
445
- ? this.hap.Characteristic.PeriodicSnapshotsActive.ENABLE
446
- : this.hap.Characteristic.PeriodicSnapshotsActive.DISABLE,
436
+ this.hap.Characteristic.HomeKitCameraActive,
437
+ this.hap.Characteristic.HomeKitCameraActive.ON,
447
438
  );
448
439
  }
449
440
  }
@@ -499,7 +490,7 @@ export default class NestCamera extends HomeKitDevice {
499
490
  typeof this.motionServices?.[zoneID]?.service === 'object' &&
500
491
  this.motionServices[zoneID].service.getCharacteristic(this.hap.Characteristic.MotionDetected).value !== true
501
492
  ) {
502
- // Trigger motion for matching zone of not aleady active
493
+ // Trigger motion for matching zone if not already active
503
494
  this.motionServices[zoneID].service.updateCharacteristic(this.hap.Characteristic.MotionDetected, true);
504
495
 
505
496
  // Log motion started into history
@@ -514,10 +505,10 @@ export default class NestCamera extends HomeKitDevice {
514
505
  this.motionTimer = setTimeout(() => {
515
506
  event.zone_ids.forEach((zoneID) => {
516
507
  if (typeof this.motionServices?.[zoneID]?.service === 'object') {
517
- // Mark associted motion services as motion not detected
508
+ // Mark associated motion services as motion not detected
518
509
  this.motionServices[zoneID].service.updateCharacteristic(this.hap.Characteristic.MotionDetected, false);
519
510
 
520
- // Log motion started into history
511
+ // Log motion stopped into history
521
512
  this.history(this.motionServices[zoneID].service, { status: 0 });
522
513
  }
523
514
  });
@@ -689,9 +680,9 @@ export default class NestCamera extends HomeKitDevice {
689
680
  // video is pipe #1
690
681
  // audio is pipe #3 if including audio
691
682
  this?.log?.debug?.(
692
- 'ffmpeg process for recording stream from "%s" will be called using the following commandline',
683
+ 'ffmpeg process for recording stream from "%s" will be called using the following commandline: %s',
693
684
  this.deviceData.description,
694
- commandLine.join(' ').toString(),
685
+ commandLine.join(' '),
695
686
  );
696
687
 
697
688
  let ffmpegStream = this.ffmpeg.createSession(
@@ -722,7 +713,7 @@ export default class NestCamera extends HomeKitDevice {
722
713
  while (buffer.length >= 8) {
723
714
  let boxSize = buffer.readUInt32BE(0);
724
715
  if (boxSize < 8 || buffer.length < boxSize) {
725
- // We dont have enough data in the buffer yet to process the full mp4 box
716
+ // We don't have enough data in the buffer yet to process the full mp4 box
726
717
  // so, exit loop and await more data
727
718
  break;
728
719
  }
@@ -900,7 +891,7 @@ export default class NestCamera extends HomeKitDevice {
900
891
 
901
892
  if (imageBuffer === undefined) {
902
893
  // If we get here, we have no snapshot image
903
- // We'll use the last success snapshop as long as its within a certain time period
894
+ // We'll use the last successful snapshot as long as its within a certain time period
904
895
  imageBuffer = this.#lastSnapshotImage;
905
896
  }
906
897
 
@@ -1024,7 +1015,7 @@ export default class NestCamera extends HomeKitDevice {
1024
1015
  '0:v:0',
1025
1016
  '-codec:v',
1026
1017
  'copy',
1027
- // Below is comment out as we don't use hardware acceleration for live streaming
1018
+ // Below is commented out as we don't use hardware acceleration for live streaming
1028
1019
  // ...(this.deviceData.ffmpeg.hwaccel === true && this.ffmpeg.hardwareH264Codec !== undefined
1029
1020
  // ? ['-codec:v', this.ffmpeg.hardwareH264Codec]
1030
1021
  // : ['-codec:v', 'copy']),
@@ -1099,9 +1090,9 @@ export default class NestCamera extends HomeKitDevice {
1099
1090
  ];
1100
1091
 
1101
1092
  this?.log?.debug?.(
1102
- 'ffmpeg process for live streaming from "%s" will be called using the following commandline',
1093
+ 'ffmpeg process for live streaming from "%s" will be called using the following commandline: %s',
1103
1094
  this.deviceData.description,
1104
- commandLine.join(' ').toString(),
1095
+ commandLine.join(' '),
1105
1096
  );
1106
1097
 
1107
1098
  // Launch the ffmpeg process for streaming and connect it to streamer input/output
@@ -1169,7 +1160,7 @@ export default class NestCamera extends HomeKitDevice {
1169
1160
  ];
1170
1161
 
1171
1162
  this?.log?.debug?.(
1172
- 'ffmpeg process for talkback on "%s" will be called using the following commandline',
1163
+ 'ffmpeg process for talkback on "%s" will be called using the following commandline: %s',
1173
1164
  this.deviceData.description,
1174
1165
  talkbackCommandLine.join(' '),
1175
1166
  );
@@ -1534,7 +1525,7 @@ export function processRawData(log, rawData, config, deviceType = undefined) {
1534
1525
  (value.value?.properties?.['cc2migration.overview_state'] === 'NORMAL' ||
1535
1526
  value.value?.properties?.['cc2migration.overview_state'] === 'REVERSE_MIGRATION_IN_PROGRESS')
1536
1527
  ) {
1537
- // We'll only use the Nest API data for Camera's which have NOT been migrated to Google Home
1528
+ // We'll only use the Nest API data for Cameras which have NOT been migrated to Google Home
1538
1529
  tempDevice = processCommonData(
1539
1530
  object_key,
1540
1531
  {
@@ -1598,7 +1589,9 @@ export function processRawData(log, rawData, config, deviceType = undefined) {
1598
1589
  );
1599
1590
  // Insert any extra options we've read in from configuration file for this device
1600
1591
  tempDevice.eveHistory = config.options.eveHistory === true || deviceOptions?.eveHistory === true;
1601
- tempDevice.hksv = (config.options?.hksv === true || deviceOptions?.hksv === true) && config.options?.ffmpeg?.valid === true;
1592
+ tempDevice.hksv =
1593
+ (deviceOptions?.hksv === true || (deviceOptions?.hksv !== false && config.options?.hksv === true)) &&
1594
+ config.options?.ffmpeg?.valid === true;
1602
1595
  tempDevice.doorbellCooldown = parseDurationToSeconds(deviceOptions?.doorbellCooldown, { defaultValue: 60, min: 0, max: 300 });
1603
1596
  tempDevice.motionCooldown = parseDurationToSeconds(deviceOptions?.motionCooldown, { defaultValue: 60, min: 0, max: 300 });
1604
1597
  tempDevice.personCooldown = parseDurationToSeconds(deviceOptions?.personCooldown, { defaultValue: 120, min: 0, max: 300 });
@@ -13,7 +13,7 @@ export { processRawData };
13
13
 
14
14
  export default class NestDoorbell extends NestCamera {
15
15
  static TYPE = 'Doorbell';
16
- static VERSION = '2025.08.12'; // Code version
16
+ static VERSION = '2025.09.08'; // Code version
17
17
 
18
18
  doorbellTimer = undefined; // Cooldown timer for doorbell events
19
19
  switchService = undefined; // HomeKit switch for enabling/disabling chime
@@ -28,13 +28,13 @@ export default class NestDoorbell extends NestCamera {
28
28
  // Setup HomeKit doorbell controller
29
29
  if (this.controller === undefined) {
30
30
  // Establish the "camera" controller here as a doorbell specific one
31
- // when onAdd is called for the base camera class, this will cconfigure our camera controller established here
31
+ // when onAdd is called for the base camera class, this will configure our camera controller established here
32
32
  this.controller = new this.hap.DoorbellController(this.generateControllerOptions());
33
33
  }
34
34
 
35
35
  if (this.deviceData?.has_indoor_chime === true && this.deviceData?.chimeSwitch === true) {
36
36
  // Add service to allow automation and enabling/disabling indoor chiming.
37
- // This needs to be explically enabled via a configuration option for the device
37
+ // This needs to be explicitly enabled via a configuration option for the device
38
38
  this.switchService = this.addHKService(this.hap.Service.Switch, '', 1);
39
39
 
40
40
  // Setup set callback for this switch service
@@ -44,7 +44,7 @@ export default class NestDoorbell extends NestCamera {
44
44
  // only change indoor chime status value if different than on-device
45
45
  this.message(NestDoorbell.SET, { uuid: this.deviceData.nest_google_uuid, indoor_chime_enabled: value });
46
46
 
47
- this?.log?.info?.('Indoor chime on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
47
+ this?.log?.info?.('Indoor chime on "%s" was turned %s', this.deviceData.description, value === true ? 'on' : 'off');
48
48
  }
49
49
  },
50
50
  onGet: () => {
@@ -10,14 +10,14 @@ export { processRawData };
10
10
 
11
11
  export default class NestFloodlight extends NestCamera {
12
12
  static TYPE = 'FloodlightCamera';
13
- static VERSION = '2025.07.25'; // Code version
13
+ static VERSION = '2025.09.08'; // Code version
14
14
 
15
15
  lightService = undefined; // HomeKit light
16
16
 
17
17
  // Class functions
18
18
  onAdd() {
19
19
  if (this.deviceData.has_light === true) {
20
- // Add service to for a light, including brightness control
20
+ // Add service for a light, including brightness control
21
21
  this.lightService = this.addHKService(this.hap.Service.Lightbulb, '', 1);
22
22
  this.addHKCharacteristic(this.lightService, this.hap.Characteristic.Brightness, {
23
23
  props: { minStep: 10 }, // Light only goes in 10% increments
@@ -38,7 +38,7 @@ export default class NestFloodlight extends NestCamera {
38
38
  if (value !== this.deviceData.light_enabled) {
39
39
  this.message(NestFloodlight.SET, { uuid: this.deviceData.nest_google_uuid, light_enabled: value });
40
40
 
41
- this?.log?.info?.('Floodlight on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
41
+ this?.log?.info?.('Floodlight on "%s" was turned %s', this.deviceData.description, value === true ? 'on' : 'off');
42
42
  }
43
43
  },
44
44
  onGet: () => {
@@ -13,7 +13,7 @@ import { DATA_SOURCE, DEVICE_TYPE, PROTOBUF_RESOURCES, LOW_BATTERY_LEVEL } from
13
13
 
14
14
  export default class NestLock extends HomeKitDevice {
15
15
  static TYPE = 'Lock';
16
- static VERSION = '2025.08.13'; // Code version
16
+ static VERSION = '2025.09.08'; // Code version
17
17
 
18
18
  // Define lock bolt states
19
19
  static STATE = {
@@ -186,7 +186,7 @@ export default class NestLock extends HomeKitDevice {
186
186
  ? deviceData.bolt_state === NestLock.STATE.LOCKED || deviceData.bolt_state === NestLock.STATE.LOCKING
187
187
  ? this.hap.Characteristic.LockLastKnownAction.SECURED_BY_KEYPAD
188
188
  : this.hap.Characteristic.LockLastKnownAction.UNSECURED_BY_KEYPAD
189
- : deviceData.bolt_actor === NestLock.LAST_ACTION.REMOTE || deviceData.LAST_ACTION === NestLock.LAST_ACTION.VOICE
189
+ : deviceData.bolt_actor === NestLock.LAST_ACTION.REMOTE || deviceData.bolt_actor === NestLock.LAST_ACTION.VOICE
190
190
  ? deviceData.bolt_state === NestLock.STATE.LOCKED || deviceData.bolt_state === NestLock.STATE.LOCKING
191
191
  ? this.hap.Characteristic.LockLastKnownAction.SECURED_REMOTELY
192
192
  : this.hap.Characteristic.LockLastKnownAction.UNSECURED_REMOTELY
@@ -13,7 +13,7 @@ import { LOW_BATTERY_LEVEL, DATA_SOURCE, PROTOBUF_RESOURCES, DEVICE_TYPE } from
13
13
 
14
14
  export default class NestProtect extends HomeKitDevice {
15
15
  static TYPE = 'Protect';
16
- static VERSION = '2025.08.04'; // Code version
16
+ static VERSION = '2025.09.08'; // Code version
17
17
 
18
18
  batteryService = undefined;
19
19
  smokeService = undefined;
@@ -109,7 +109,7 @@ export default class NestProtect extends HomeKitDevice {
109
109
  }
110
110
 
111
111
  if (deviceData.smoke_status === false && this.deviceData.smoke_status === true) {
112
- this?.log?.info?.('Smoke is nolonger detected in "%s"', deviceData.description);
112
+ this?.log?.info?.('Smoke is no longer detected in "%s"', deviceData.description);
113
113
  }
114
114
 
115
115
  // Update carbon monoxide details
@@ -13,7 +13,7 @@ import { LOW_BATTERY_LEVEL, DATA_SOURCE, PROTOBUF_RESOURCES, DEVICE_TYPE } from
13
13
 
14
14
  export default class NestTemperatureSensor extends HomeKitDevice {
15
15
  static TYPE = 'TemperatureSensor';
16
- static VERSION = '2025.08.04'; // Code version
16
+ static VERSION = '2025.09.08'; // Code version
17
17
 
18
18
  batteryService = undefined;
19
19
  temperatureService = undefined;
@@ -50,7 +50,7 @@ export default class NestTemperatureSensor extends HomeKitDevice {
50
50
 
51
51
  this.temperatureService.updateCharacteristic(this.hap.Characteristic.StatusActive, deviceData.online === true);
52
52
  if (typeof deviceData?.associated_thermostat === 'string' && deviceData.associated_thermostat !== '') {
53
- // This temperature sensor is assocated with a thermostat
53
+ // This temperature sensor is associated with a thermostat
54
54
  // Update status if providing active temperature for the thermostats
55
55
  this.temperatureService.updateCharacteristic(
56
56
  this.hap.Characteristic.StatusActive,
@@ -28,7 +28,7 @@ import {
28
28
 
29
29
  export default class NestThermostat extends HomeKitDevice {
30
30
  static TYPE = 'Thermostat';
31
- static VERSION = '2025.08.21'; // Code version
31
+ static VERSION = '2025.09.08'; // Code version
32
32
 
33
33
  thermostatService = undefined;
34
34
  batteryService = undefined;
@@ -205,7 +205,7 @@ export default class NestThermostat extends HomeKitDevice {
205
205
  this.fanService = undefined;
206
206
  }
207
207
 
208
- // Setup dehumifider service if supported by the thermostat and not already present on the accessory
208
+ // Setup dehumidifier service if supported by the thermostat and not already present on the accessory
209
209
  if (this.deviceData?.has_dehumidifier === true) {
210
210
  this.#setupDehumidifier();
211
211
  }
@@ -218,7 +218,7 @@ export default class NestThermostat extends HomeKitDevice {
218
218
  this.dehumidifierService = undefined;
219
219
  }
220
220
 
221
- // Setup humdity service if configured to be seperate and not already present on the accessory
221
+ // Setup humidity service if configured to be separate and not already present on the accessory
222
222
  if (this.deviceData?.humiditySensor === true) {
223
223
  this.humidityService = this.addHKService(this.hap.Service.HumiditySensor, '', 1);
224
224
  this.thermostatService.addLinkedService(this.humidityService);
@@ -230,7 +230,7 @@ export default class NestThermostat extends HomeKitDevice {
230
230
  });
231
231
  }
232
232
  if (this.deviceData?.humiditySensor === false) {
233
- // No longer have a seperate humidity sensor configure and service present, so removed it
233
+ // No longer have a separate humidity sensor configure and service present, so removed it
234
234
  this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor);
235
235
  if (this.humidityService !== undefined) {
236
236
  this.accessory.removeService(this.humidityService);
@@ -239,7 +239,7 @@ export default class NestThermostat extends HomeKitDevice {
239
239
  }
240
240
 
241
241
  // Attempt to load any external modules for this thermostat
242
- // We support external cool/heat/fan/dehumidifier/humdifier module functions
242
+ // We support external cool/heat/fan/dehumidifier/humidifier module functions
243
243
  // This is all undocumented on how to use, as its for my specific use case :-)
244
244
  this.externalCool = await this.#loadExternalModule(this.deviceData?.externalCool, ['cool', 'off']);
245
245
  this.externalHeat = await this.#loadExternalModule(this.deviceData?.externalHeat, ['heat', 'off']);
@@ -248,7 +248,7 @@ export default class NestThermostat extends HomeKitDevice {
248
248
  this.externalHumidifier = await this.#loadExternalModule(this.deviceData?.externalHumidifier, ['humidifier', 'off']);
249
249
 
250
250
  // Extra setup details for output
251
- this.humidityService !== undefined && this.postSetupDetail('Seperate humidity sensor');
251
+ this.humidityService !== undefined && this.postSetupDetail('Separate humidity sensor');
252
252
  this.externalCool !== undefined && this.postSetupDetail('Using external cooling module');
253
253
  this.externalHeat !== undefined && this.postSetupDetail('Using external heating module');
254
254
  this.externalFan !== undefined && this.postSetupDetail('Using external fan module');
@@ -351,7 +351,7 @@ export default class NestThermostat extends HomeKitDevice {
351
351
  : this.hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED,
352
352
  );
353
353
 
354
- // Update seperate humidity sensor if configured todo so
354
+ // Update separate humidity sensor if configured to do so
355
355
  if (this.humidityService !== undefined) {
356
356
  this.humidityService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, deviceData.current_humidity);
357
357
  }
@@ -378,14 +378,14 @@ export default class NestThermostat extends HomeKitDevice {
378
378
  );
379
379
  }
380
380
 
381
- // Check for dehumidifer setup change on thermostat
381
+ // Check for dehumidifier setup change on thermostat
382
382
  if (deviceData.has_dehumidifier !== this.deviceData.has_dehumidifier) {
383
383
  if (deviceData.has_dehumidifier === true && this.deviceData.has_dehumidifier === false && this.dehumidifierService === undefined) {
384
384
  // Dehumidifier has been added
385
385
  this.#setupDehumidifier();
386
386
  }
387
387
  if (deviceData.has_dehumidifier === false && this.deviceData.has_dehumidifier === true && this.dehumidifierService !== undefined) {
388
- // Dehumidifer has been removed
388
+ // Dehumidifier has been removed
389
389
  this.accessory.removeService(this.dehumidifierService);
390
390
  this.dehumidifierService = undefined;
391
391
  }
@@ -529,7 +529,7 @@ export default class NestThermostat extends HomeKitDevice {
529
529
  }
530
530
  if (deviceData.hvac_state.toUpperCase() === 'OFF') {
531
531
  if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && typeof this.externalCool?.off === 'function') {
532
- // Switched to off mode and external cooling external code was being used, so stop cooling via cooling external code{
532
+ // Switched to off mode and external cooling external code was being used, so stop cooling via cooling external code
533
533
  this.externalCool.off();
534
534
  }
535
535
  if (this.deviceData.hvac_state.toUpperCase() === 'HEATING' && typeof this.externalHeat?.off === 'function') {
@@ -696,11 +696,11 @@ export default class NestThermostat extends HomeKitDevice {
696
696
  setFan(fanState, speed) {
697
697
  let currentState = this.fanService.getCharacteristic(this.hap.Characteristic.Active).value;
698
698
 
699
- // If we have a rotation speed characteristic, use that get the current fan speed, otherwise we us ethe current fan state to determine
699
+ // If we have a rotation speed characteristic, use that get the current fan speed, otherwise we use the current fan state to determine
700
700
  let currentSpeed =
701
701
  this.fanService.testCharacteristic(this.hap.Characteristic.RotationSpeed) === true
702
702
  ? this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).value
703
- : currentState === true
703
+ : currentState === this.hap.Characteristic.Active.ACTIVE
704
704
  ? 100
705
705
  : 0;
706
706
 
@@ -756,18 +756,18 @@ export default class NestThermostat extends HomeKitDevice {
756
756
  }
757
757
  }
758
758
 
759
- setDehumidifier(dehumidiferState) {
760
- let isActive = dehumidiferState === this.hap.Characteristic.Active.ACTIVE;
759
+ setDehumidifier(dehumidifierState) {
760
+ let isActive = dehumidifierState === this.hap.Characteristic.Active.ACTIVE;
761
761
 
762
762
  this.message(HomeKitDevice.SET, {
763
763
  uuid: this.deviceData.nest_google_uuid,
764
764
  dehumidifier_state: isActive,
765
765
  });
766
766
 
767
- this.dehumidifierService.updateCharacteristic(this.hap.Characteristic.Active, dehumidiferState);
767
+ this.dehumidifierService.updateCharacteristic(this.hap.Characteristic.Active, dehumidifierState);
768
768
 
769
769
  this?.log?.info?.(
770
- 'Set dehumidifer on thermostat "%s" to "%s"',
770
+ 'Set dehumidifier on thermostat "%s" to "%s"',
771
771
  this.deviceData.description,
772
772
  isActive ? 'On with target humidity level of ' + this.deviceData.target_humidity + '%' : 'Off',
773
773
  );
@@ -989,7 +989,7 @@ export default class NestThermostat extends HomeKitDevice {
989
989
  props: { minStep: 100 / this.deviceData.fan_max_speed },
990
990
  onSet: (value) => this.setFan(value !== 0 ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE, value),
991
991
  onGet: () => {
992
- return this.deviceData.fanState === true ? (this.deviceData.fan_timer_speed / this.deviceData.fan_max_speed) * 100 : 0;
992
+ return this.deviceData.fan_state === true ? (this.deviceData.fan_timer_speed / this.deviceData.fan_max_speed) * 100 : 0;
993
993
  },
994
994
  });
995
995
  } else {
@@ -1265,7 +1265,7 @@ export function processRawData(log, rawData, config, deviceType = undefined) {
1265
1265
  if (
1266
1266
  value.value?.hvac_control?.hvacState?.coolStage1Active === true ||
1267
1267
  value.value?.hvac_control?.hvacState?.coolStage2Active === true ||
1268
- value.value?.hvac_control?.hvacState?.coolStage2Active === true
1268
+ value.value?.hvac_control?.hvacState?.coolStage3Active === true
1269
1269
  ) {
1270
1270
  // A cooling source is on, so we're in cooling mode
1271
1271
  RESTTypeData.hvac_state = 'cooling';
@@ -13,7 +13,7 @@ import { DATA_SOURCE, DEVICE_TYPE, NESTLABS_MAC_PREFIX } from '../consts.js';
13
13
 
14
14
  export default class NestWeather extends HomeKitDevice {
15
15
  static TYPE = 'Weather';
16
- static VERSION = '2025.08.04'; // Code version
16
+ static VERSION = '2025.09.08'; // Code version
17
17
 
18
18
  batteryService = undefined;
19
19
  airPressureService = undefined;
@@ -155,7 +155,7 @@ export default class NestWeather extends HomeKitDevice {
155
155
  this.temperatureService.updateCharacteristic(this.hap.Characteristic.SunsetTime, dateString);
156
156
  }
157
157
 
158
- // If we have the history service running, record temperature and humity every 5mins
158
+ // If we have the history service running, record temperature and humidity every 5mins
159
159
  this.history(
160
160
  this.airPressureService,
161
161
  { temperature: deviceData.current_temperature, humidity: deviceData.current_humidity, pressure: 0 },
@@ -187,7 +187,7 @@ export function processRawData(log, rawData, config, deviceType = undefined) {
187
187
  let tempDevice = {};
188
188
  try {
189
189
  // For a Google API source data, we use the Nest API structure ID. This will ensure we generate the same serial number
190
- // This should prevent two 'wether' objects from being created
190
+ // This should prevent two 'weather' objects from being created
191
191
  // Nest API uses the structure id only
192
192
  let serialNumber =
193
193
  value?.source === DATA_SOURCE.GOOGLE &&
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/system.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Nest System communications
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 2025.08.21
4
+ // Code version 2025.09.08
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
@@ -236,7 +236,7 @@ export default class NestAccfactory {
236
236
  if (refresh !== true) {
237
237
  this?.log?.success?.('Successfully authorised using Google account for connection "%s"', this.#connections[uuid].name);
238
238
  } else {
239
- this?.log?.debug?.('Successfully performed token refesh using Google account for connection "%s"', this.#connections[uuid].name);
239
+ this?.log?.debug?.('Successfully performed token refresh using Google account for connection "%s"', this.#connections[uuid].name);
240
240
  }
241
241
  } catch (error) {
242
242
  // Attempt to extract HTTP status code from error cause or error object
@@ -533,7 +533,7 @@ export default class NestAccfactory {
533
533
  };
534
534
  }
535
535
 
536
- // Dump the the raw data if configured todo so
536
+ // Dump the raw data if configured to do so
537
537
  // This can be used for user support, rather than specific build to dump this :-)
538
538
  if (this?.config?.options?.rawdump === true && this.#connections[uuid]?.doneNestRawDump !== true) {
539
539
  this.#connections[uuid].doneNestRawDump = true; // Done once
@@ -714,7 +714,7 @@ export default class NestAccfactory {
714
714
  }
715
715
  }
716
716
 
717
- // Dump the the raw data if configured todo so
717
+ // Dump the raw data if configured to do so
718
718
  // This can be used for user support, rather than specific build to dump this :-)
719
719
  if (this?.config?.options?.rawdump === true && this.#connections[uuid]?.doneGoogleRawDump !== true) {
720
720
  this.#connections[uuid].doneGoogleRawDump = true; // Done once
@@ -1323,7 +1323,7 @@ export default class NestAccfactory {
1323
1323
  commandTraits.push(structuredClone(commandElement));
1324
1324
  }
1325
1325
 
1326
- // Perform any direct trait updates we have todo. This can be done via a single call in a batch
1326
+ // Perform any direct trait updates we have to do. This can be done via a single call in a batch
1327
1327
  if (updatedTraits.length !== 0) {
1328
1328
  let commandResponse = await this.#protobufCommand(uuid, 'nestlabs.gateway.v1.TraitBatchApi', 'BatchUpdateState', {
1329
1329
  batchUpdateStateRequest: updatedTraits,
@@ -1335,7 +1335,7 @@ export default class NestAccfactory {
1335
1335
  this?.log?.debug?.('Google API had error updating traits for device uuid "%s"', nest_google_uuid);
1336
1336
  }
1337
1337
  }
1338
- // Perform any trait updates required via resource commands. Each one is done seperately
1338
+ // Perform any trait updates required via resource commands. Each one is done separately
1339
1339
  if (commandTraits.length !== 0) {
1340
1340
  for (let command of commandTraits) {
1341
1341
  let commandResponse = await this.#protobufCommand(uuid, 'nestlabs.gateway.v1.ResourceApi', 'SendCommand', command);
@@ -2065,7 +2065,7 @@ export default class NestAccfactory {
2065
2065
  }
2066
2066
  };
2067
2067
 
2068
- // Retrieve both 'Request' and 'Reponse' traits for the associated service and command
2068
+ // Retrieve both 'Request' and 'Response' traits for the associated service and command
2069
2069
  service = service.trim();
2070
2070
  command = command.trim();
2071
2071
  let TraitMapService = this.#protobufRoot.lookup(service);
@@ -2191,7 +2191,7 @@ export default class NestAccfactory {
2191
2191
  return;
2192
2192
  }
2193
2193
 
2194
- // Attempt to load in requifred protobuf files
2194
+ // Attempt to load in required protobuf files
2195
2195
  if (fs.existsSync(path.join(__dirname, 'protobuf/root.proto')) === true) {
2196
2196
  protobuf.util.Long = null;
2197
2197
  protobuf.configure();
@@ -2203,7 +2203,7 @@ export default class NestAccfactory {
2203
2203
 
2204
2204
  if (this.#protobufRoot === null) {
2205
2205
  this?.log?.warn?.(
2206
- 'Failed to loaded protobuf support files for Google API. This will cause certain Nest/Google devices to be un-supported',
2206
+ 'Failed to load protobuf support files for Google API. This will cause certain Nest/Google devices to be unsupported',
2207
2207
  );
2208
2208
  }
2209
2209
  }
package/dist/utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // General helper functions
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 2025.08.20
4
+ // Code version 2025.09.08
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
@@ -134,7 +134,6 @@ async function fetchWrapper(method, url, options, data) {
134
134
 
135
135
  let delay = 500 * Math.pow(2, options._retryCount - 1);
136
136
  await new Promise((resolve) => {
137
- resolve = resolve;
138
137
  setTimeout(resolve, delay);
139
138
  });
140
139
 
package/dist/webrtc.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // Handles connection and data from Google WebRTC systems
5
5
  // Currently a "work in progress"
6
6
  //
7
- // Code version 2025.07.23
7
+ // Code version 2025.09.08
8
8
  // Mark Hulskamp
9
9
  'use strict';
10
10
 
@@ -105,7 +105,7 @@ export default class WebRTC extends Streamer {
105
105
  });
106
106
 
107
107
  // Translate our uuid (DEVICE_xxxxxxxxxx) into the associated 'google id' from the Google Home Foyer
108
- // We need this id for SOME calls to Google Home Foyer services. Gotta love consistancy :-)
108
+ // We need this id for SOME calls to Google Home Foyer services. Gotta love consistency :-)
109
109
  if (homeFoyerResponse?.data?.[0]?.homes !== undefined) {
110
110
  Object.values(homeFoyerResponse?.data?.[0]?.homes).forEach((home) => {
111
111
  Object.values(home.devices).forEach((device) => {
@@ -273,7 +273,7 @@ export default class WebRTC extends Streamer {
273
273
  });
274
274
 
275
275
  if (homeFoyerResponse?.data?.[0]?.streamExtensionStatus !== 'STATUS_STREAM_EXTENDED') {
276
- this?.log?.debug?.('Error occurred while requested stream extension for uuid "%s"', this.nest_google_uuid);
276
+ this?.log?.debug?.('Error occurred while requesting stream extension for uuid "%s"', this.nest_google_uuid);
277
277
 
278
278
  await this.#peerConnection?.close?.();
279
279
  }
@@ -371,7 +371,7 @@ export default class WebRTC extends Streamer {
371
371
  }
372
372
  if (homeFoyerResponse?.status === 0) {
373
373
  this.audio.talking = true;
374
- this?.log?.debug?.('Talking start on uuid "%s"', this.nest_google_uuid);
374
+ this?.log?.debug?.('Talking started on uuid "%s"', this.nest_google_uuid);
375
375
  }
376
376
  }
377
377
 
@@ -383,14 +383,14 @@ export default class WebRTC extends Streamer {
383
383
  rtpHeader.payloadOffset = RTP_PACKET_HEADER_SIZE;
384
384
  rtpHeader.payloadType = this.audio.id; // As the camera is send/recv, we use the same payload type id as the incoming audio
385
385
  rtpHeader.timestamp = Date.now() >>> 0; // Think the time stamp difference should be 960ms per audio packet?
386
- rtpHeader.sequenceNumber = this.audio.talkSquenceNumber++ & 0xffff;
386
+ rtpHeader.sequenceNumber = this.audio.talkSequenceNumber++ & 0xffff;
387
387
  let rtpPacket = new werift.RtpPacket(rtpHeader, talkingBuffer);
388
388
  this.#audioTransceiver.sender.sendRtp(rtpPacket.serialize());
389
389
  }
390
390
  }
391
391
 
392
392
  if (talkingBuffer.length === 0 && this.audio?.talking === true) {
393
- // Buffer length of zero, ised to signal no more talking data for the moment
393
+ // Buffer length of zero, used to signal no more talking data for the moment
394
394
  let homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'SendTalkback', {
395
395
  googleDeviceId: {
396
396
  value: this.#googleHomeDeviceUUID,
@@ -421,7 +421,7 @@ export default class WebRTC extends Streamer {
421
421
  baseTime: undefined,
422
422
  sampleRate: 48000,
423
423
  opus: undefined,
424
- talkSquenceNumber: weriftTrack?.sender?.sequenceNumber === undefined ? 0 : weriftTrack.sender.sequenceNumber,
424
+ talkSequenceNumber: weriftTrack?.sender?.sequenceNumber === undefined ? 0 : weriftTrack.sender.sequenceNumber,
425
425
  talking: undefined,
426
426
  };
427
427
  }
@@ -450,7 +450,7 @@ export default class WebRTC extends Streamer {
450
450
  return;
451
451
  }
452
452
 
453
- // Create timer for stalled rtp output. Restart strem if so
453
+ // Create timer for stalled rtp output. Restart stream if so
454
454
  clearTimeout(this.#stalledTimer);
455
455
  this.#stalledTimer = setTimeout(async () => {
456
456
  await this.#peerConnection?.close?.();
@@ -604,7 +604,7 @@ export default class WebRTC extends Streamer {
604
604
  return;
605
605
  }
606
606
 
607
- // Attempt to retrieve both 'Request' and 'Reponse' traits for the associated service and command
607
+ // Attempt to retrieve both 'Request' and 'Response' traits for the associated service and command
608
608
  let TraitMapRequest = this.#protobufFoyer.lookup(GOOGLE_HOME_FOYER_PREFIX + command + 'Request');
609
609
  let TraitMapResponse = this.#protobufFoyer.lookup(GOOGLE_HOME_FOYER_PREFIX + command + 'Response');
610
610
  let buffer = Buffer.alloc(0);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "homebridge-nest-accfactory",
3
3
  "displayName": "Nest Accfactory",
4
4
  "type": "module",
5
- "version": "0.3.3",
5
+ "version": "0.3.4",
6
6
  "description": "Homebridge support for Nest/Google devices including HomeKit Secure Video (HKSV) support for doorbells and cameras",
7
7
  "author": "n0rt0nthec4t",
8
8
  "license": "Apache-2.0",
@@ -54,11 +54,11 @@
54
54
  "prepublishOnly": "npm run lint && npm run build"
55
55
  },
56
56
  "devDependencies": {
57
- "@eslint/js": "^9.34.0",
58
- "eslint": "^9.34.0",
59
- "@stylistic/eslint-plugin": "^5.2.3",
60
- "@types/node": "^24.3.0",
61
- "@typescript-eslint/parser": "^8.40.0",
57
+ "@eslint/js": "^9.37.0",
58
+ "eslint": "^9.37.0",
59
+ "@stylistic/eslint-plugin": "^5.4.0",
60
+ "@types/node": "^24.8.1",
61
+ "@typescript-eslint/parser": "^8.46.1",
62
62
  "prettier": "^3.6.2",
63
63
  "prettier-eslint": "^16.4.2",
64
64
  "copyfiles": "^2.4.1",
@@ -69,6 +69,6 @@
69
69
  "@evan/opus": "^1.0.3",
70
70
  "protobufjs": "^7.5.4",
71
71
  "werift": "^0.22.2",
72
- "undici": "7.15.0"
72
+ "undici": "7.16.0"
73
73
  }
74
74
  }
package/dist/rtpmuxer.js DELETED
@@ -1,186 +0,0 @@
1
- // rtpmuxer.js
2
- // Unified RTP Muxer + Stream Engine with FFmpeg support
3
- // Part of homebridge-nest-accfactory
4
- //
5
- // Code version 2025.07.04
6
- // Mark Hulskamp
7
- 'use strict';
8
-
9
- // Define nodejs module requirements
10
- import dgram from 'dgram';
11
- import { Writable } from 'stream';
12
- import { Buffer } from 'node:buffer';
13
- import { setInterval, clearInterval } from 'node:timers';
14
-
15
- // Define constants
16
- const LOG_LEVELS = {
17
- INFO: 'info',
18
- SUCCESS: 'success',
19
- WARN: 'warn',
20
- ERROR: 'error',
21
- DEBUG: 'debug',
22
- };
23
- const RTP_PACKET_HEADER_SIZE = 12;
24
-
25
- export default class RTPMuxer {
26
- static RTP_PORT_START = 50000;
27
- static RTP_PORT_END = 51000;
28
- static SAMPLE_RATE_VIDEO = 90000;
29
- static SAMPLE_RATE_AUDIO = 48000;
30
- static PAYLOAD_TYPE_H264 = 96;
31
- static PAYLOAD_TYPE_OPUS = 111;
32
-
33
- static STREAM_TYPE = {
34
- BUFFER: 'buffer',
35
- LIVE: 'live',
36
- RECORD: 'record',
37
- TALK: 'talk',
38
- };
39
-
40
- log = undefined; // Logging function object
41
-
42
- #udpServer = undefined; // UDP server for RTP packets
43
- #port = undefined; // UDP port for RTP packets
44
- #outputSessions = new Map(); // Output sessions for RTP streams
45
- #buffer = []; // Buffer for RTP packets
46
- #bufferDuration = 5000;
47
- #bufferTimer = undefined; // Timer for buffer cleanup
48
- #ffmpeg = undefined; // FFmpeg instance for processing RTP streams
49
-
50
- constructor(options) {
51
- // Setup logger object if passed as option
52
- if (Object.values(LOG_LEVELS).every((fn) => typeof options?.log?.[fn] === 'function')) {
53
- this.log = options.log;
54
- }
55
-
56
- this.#ffmpeg = options.ffmpeg; // pass instance of FFmpeg from ffmpeg.js
57
- }
58
-
59
- async start() {
60
- this.#port = await this.#allocatePort();
61
- this.#udpServer = dgram.createSocket('udp4');
62
- this.#udpServer.on('message', (msg) => this.#handleRTP(msg));
63
- this.#udpServer.bind(this.#port);
64
- this.#startBufferLoop();
65
- }
66
-
67
- stop(uuid) {
68
- if (this.#udpServer) {
69
- this.#udpServer.close();
70
- this.#udpServer = undefined;
71
- }
72
-
73
- clearInterval(this.#bufferTimer);
74
- this.#outputSessions.clear();
75
-
76
- this.#ffmpeg?.killAllSessions?.(uuid);
77
- }
78
-
79
- getPort() {
80
- return this.#port;
81
- }
82
-
83
- getSDP(kind) {
84
- let sdp = '';
85
- if (kind === 'video') {
86
- sdp += 'm=video ' + this.#port + ' RTP/AVP ' + RTPMuxer.PAYLOAD_TYPE_H264 + '\r\n';
87
- sdp += 'a=rtpmap:' + RTPMuxer.PAYLOAD_TYPE_H264 + ' H264/' + RTPMuxer.SAMPLE_RATE_VIDEO + '\r\n';
88
- } else if (kind === 'audio') {
89
- sdp += 'm=audio ' + this.#port + ' RTP/AVP ' + RTPMuxer.PAYLOAD_TYPE_OPUS + '\r\n';
90
- sdp += 'a=rtpmap:' + RTPMuxer.PAYLOAD_TYPE_OPUS + ' opus/' + RTPMuxer.SAMPLE_RATE_AUDIO + '/2\r\n';
91
- }
92
- return sdp;
93
- }
94
-
95
- attachOutput(sessionID, writableStream, options = {}) {
96
- this.#outputSessions.set(sessionID, {
97
- stream: writableStream,
98
- kind: options.kind,
99
- isRecording: options.isRecording === true,
100
- });
101
- }
102
-
103
- detachOutput(sessionID) {
104
- this.#outputSessions.delete(sessionID);
105
- }
106
-
107
- getWritableStream(type) {
108
- return new Writable({
109
- write: (chunk, encoding, callback) => {
110
- if (type === RTPMuxer.STREAM_TYPE.BUFFER) {
111
- this.#buffer.push({ time: Date.now(), data: chunk });
112
- }
113
- for (let session of this.#outputSessions.values()) {
114
- if (session.kind === type || session.kind === undefined) {
115
- session.stream.write(chunk);
116
- }
117
- }
118
- callback();
119
- },
120
- });
121
- }
122
-
123
- getBufferedPackets(kind) {
124
- let now = Date.now();
125
- return this.#buffer.filter((p) => p.kind === kind && now - p.timestamp <= this.#bufferDuration).map((p) => p.packet);
126
- }
127
-
128
- startSession(uuid, sessionID, args, sessionType = 'live', errorCallback, pipeCount = 4) {
129
- return this.#ffmpeg?.createSession?.(uuid, sessionID, args, sessionType, errorCallback, pipeCount);
130
- }
131
-
132
- stopSession(uuid, sessionID, sessionType = 'live') {
133
- return this.#ffmpeg?.killSession?.(uuid, sessionID, sessionType);
134
- }
135
-
136
- processRTP(packet) {
137
- this.#handleRTP(packet);
138
- }
139
-
140
- #handleRTP(packet) {
141
- if (Buffer.isBuffer(packet) === false || packet.length < RTP_PACKET_HEADER_SIZE) {
142
- return;
143
- }
144
-
145
- let payloadType = packet[1] & 0x7f;
146
- let kind = payloadType === RTPMuxer.PAYLOAD_TYPE_H264 ? 'video' : payloadType === RTPMuxer.PAYLOAD_TYPE_OPUS ? 'audio' : undefined;
147
- if (kind === undefined) {
148
- return;
149
- }
150
-
151
- let copy = Buffer.from(packet);
152
- this.#buffer.push({ kind, timestamp: Date.now(), packet: copy });
153
-
154
- for (let session of this.#outputSessions.values()) {
155
- if (session.kind === kind || session.kind === undefined) {
156
- session.stream.write(copy);
157
- }
158
- }
159
- }
160
-
161
- #startBufferLoop() {
162
- this.#bufferTimer = setInterval(() => {
163
- let now = Date.now();
164
- this.#buffer = this.#buffer.filter((p) => now - p.timestamp <= this.#bufferDuration);
165
- }, 1000);
166
- }
167
-
168
- async #allocatePort() {
169
- for (let port = RTPMuxer.RTP_PORT_START; port <= RTPMuxer.RTP_PORT_END; port += 2) {
170
- try {
171
- await new Promise((resolve, reject) => {
172
- let socket = dgram.createSocket('udp4');
173
- socket.once('error', reject);
174
- socket.bind(port, () => {
175
- socket.close();
176
- resolve();
177
- });
178
- });
179
- return port;
180
- } catch {
181
- // try next port
182
- }
183
- }
184
- throw new Error('No available UDP port');
185
- }
186
- }