homebridge-nest-accfactory 0.2.3 → 0.2.9

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,21 @@
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.2.8 (2025/03/23)
6
+
7
+ - General code cleanup and bug fixes
8
+ - Support for Nest Protect(s) in Google Home app
9
+ - Default location to check for ffmpeg binary is now /usr/local/bin
10
+ - Logs Nest Protect(s) self testing status
11
+
12
+ ## v0.2.5 (2024/12/10)
13
+
14
+ - Fix for dropped sub modules.. Do not know why!
15
+
16
+ ## v0.2.4 (2024/12/10)
17
+
18
+ - Fix for camera video stream when audio disabled
19
+
5
20
  ## v0.2.3 (2024/12/06)
6
21
 
7
22
  - General code cleanup and bug fixes
package/README.md CHANGED
@@ -95,38 +95,36 @@ Sample config.json entries below
95
95
 
96
96
  The following options are available in the config.json options object. These apply to all discovered devices.
97
97
 
98
- | Name | Description | Default |
99
- |-------------------|-----------------------------------------------------------------------------------------------|------------|
100
- | elevation | Height above sea level for the weather station | 0 |
101
- | eveHistory | Provide history in EveHome application where applicable | true |
102
- | ffmegDebug | Turns on specific debugging output for when ffmpeg is envoked | false |
103
- | ffmegPath | Path to an ffmpeg binary for us to use. Will look in current directory by default | |
104
- | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
105
- | maxStreams | Maximum number of concurrent video streams in HomeKit for supported camera(s) and doorbell(s) | 2 |
106
- | weather | Virtual weather station for each Nest/Google home we discover | false |
98
+ | Name | Description | Default |
99
+ |-------------------|-----------------------------------------------------------------------------------------------|----------------|
100
+ | elevation | Height above sea level for the weather station | 0 |
101
+ | eveHistory | Provide history in EveHome application where applicable | true |
102
+ | ffmegDebug | Turns on specific debugging output for when ffmpeg is envoked | false |
103
+ | ffmegPath | Path to an ffmpeg binary for us to use | /usr/local/bin |
104
+ | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
105
+ | maxStreams | Maximum number of concurrent video streams in HomeKit for supported camera(s) and doorbell(s) | 2 |
106
+ | weather | Virtual weather station for each Nest/Google home we discover | false |
107
107
 
108
108
  #### devices
109
109
 
110
110
  The following options are available on a per-device level in the config.json devices object. The device is specified by using its serial number (in uppercase)
111
111
 
112
- | Name | Description | Default |
113
- |-------------------|-----------------------------------------------------------------------------------------------|------------|
114
- | chimeSwitch | Create a switch for supported doorbell(s) which allows the indoor chime to be turned on/off | false |
115
- | doorbellCooldown | Time in seconds between doorbell press events | 60 |
116
- | elevation | Height above sea level for the specific weather station | 0 |
117
- | eveHistory | Provide history in EveHome application where applicable for the specific device | true |
118
- | exclude | Exclude the device | false |
119
- | ffmegDebug | Turns on specific debugging output for when ffmpeg is envoked | false |
120
- | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
121
- | humiditySensor | Create a seperate humidity sensor for supported thermostat(s) | false |
122
- | localAccess | Use direct access to supported camera(s) and doorbell(s) for video streaming and recording | false |
123
- | motionCooldown | Time in seconds between detected motion events | 60 |
124
- | personCooldown | Time in seconds between detected person events | 120 |
112
+ | Name | Description | Default |
113
+ |-------------------|-----------------------------------------------------------------------------------------------|----------------|
114
+ | chimeSwitch | Create a switch for supported doorbell(s) which allows the indoor chime to be turned on/off | false |
115
+ | doorbellCooldown | Time in seconds between doorbell press events | 60 |
116
+ | elevation | Height above sea level for the specific weather station | 0 |
117
+ | eveHistory | Provide history in EveHome application where applicable for the specific device | true |
118
+ | exclude | Exclude the device | false |
119
+ | ffmegDebug | Turns on specific debugging output for when ffmpeg is envoked | false |
120
+ | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
121
+ | humiditySensor | Create a seperate humidity sensor for supported thermostat(s) | false |
122
+ | localAccess | Use direct access to supported camera(s) and doorbell(s) for video streaming and recording | false |
123
+ | motionCooldown | Time in seconds between detected motion events | 60 |
124
+ | personCooldown | Time in seconds between detected person events | 120 |
125
125
 
126
126
  ## ffmpeg
127
127
 
128
- **As of 3/10/2024, the [Homebridge Docker Image](https://hub.docker.com/r/homebridge/homebridge) includes an ffmpeg binary meeting our requirements. Its located in /usr/local/bin/ffmpeg**
129
-
130
128
  To support streaming and recording from cameras, an ffmpeg binary needs to be present. We have specific requirements, which are:
131
129
  - version 6.0 or later
132
130
  - compiled with:
@@ -135,7 +133,7 @@ To support streaming and recording from cameras, an ffmpeg binary needs to be pr
135
133
  - libspeex
136
134
  - libopus
137
135
 
138
- By default, we look in the current directory where the plug-in excutes for an ffmpeg binary, however, you can specify a specific ffmpeg binary to use via the configuration option 'ffmpegPath'
136
+ By default, we look in /usr/local/bin for an ffmpeg binary, however, you can specify a specific ffmpeg binary to use via the configuration option 'ffmpegPath'
139
137
 
140
138
  ## Disclaimer
141
139
 
@@ -5,6 +5,12 @@
5
5
  "schema": {
6
6
  "type": "object",
7
7
  "properties": {
8
+ "name": {
9
+ "title": "Name",
10
+ "type": "string",
11
+ "default": "NestAccfactory",
12
+ "condition": "1=2"
13
+ },
8
14
  "nest": {
9
15
  "title": "Nest Account",
10
16
  "type": "object",
@@ -90,7 +96,8 @@
90
96
  "ffmpegPath": {
91
97
  "title": "Path to ffmpeg binary",
92
98
  "type": "string",
93
- "placeholder": "Path to an ffmpeg binary. By default, we look in HomeBridge user path"
99
+ "placeholder": "Path to an ffmpeg binary",
100
+ "default": "/usr/local/bin/ffmpeg"
94
101
  }
95
102
  }
96
103
  }
@@ -10,7 +10,7 @@
10
10
  //
11
11
  // Credit to https://github.com/simont77/fakegato-history for the work on starting the EveHome comms protocol decoding
12
12
  //
13
- // Version 15/10/2024
13
+ // Version 2025/18/01
14
14
  // Mark Hulskamp
15
15
 
16
16
  // Define nodejs module requirements
@@ -1169,7 +1169,7 @@ export default class HomeKitHistory {
1169
1169
  firmware: typeof options?.EveSmoke_firmware === 'number' ? options.EveSmoke_firmware : 1208, // Firmware version
1170
1170
  lastalarmtest: typeof options?.EveSmoke_lastalarmtest === 'number' ? options.EveSmoke_lastalarmtest : 0, // Seconds of alarm test
1171
1171
  alarmtest: options?.EveSmoke_alarmtest === true, // Is alarmtest running
1172
- heatstatus: typeof options?.EveSmoke_heatstatus === 'number' ? options.EveSmoke_heatstatus : 0, // Heat sensor status
1172
+ heatstatus: options.EveSmoke_heatstatus === true, // Heat sensor status
1173
1173
  statusled: options?.EveSmoke_statusled === false, // Status LED flash/enabled
1174
1174
  smoketestpassed: options?.EveSmoke_smoketestpassed === false, // Passed smoke test?
1175
1175
  heattestpassed: options?.EveSmoke_heattestpassed === false, // Passed smoke test?
@@ -2009,7 +2009,7 @@ export default class HomeKitHistory {
2009
2009
  ) {
2010
2010
  value |= 1 << 0; // 1st bit, smoke detected
2011
2011
  }
2012
- if (this.EveSmokePersist.heatstatus !== 0) {
2012
+ if (this.EveSmokePersist.heatstatus === true) {
2013
2013
  value |= 1 << 1; // 2th bit - heat detected
2014
2014
  }
2015
2015
  if (this.EveSmokePersist.alarmtest === true) {
package/dist/camera.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Nest Cameras
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 10/11/2024
4
+ // Code version 2025/03/19
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
@@ -71,15 +71,19 @@ export default class NestCamera extends HomeKitDevice {
71
71
  }
72
72
 
73
73
  // Class functions
74
- addServices() {
74
+ addServices(hapController = this.hap.CameraController) {
75
75
  // Setup motion services
76
76
  if (this.motionServices === undefined) {
77
77
  this.createCameraMotionServices();
78
78
  }
79
79
 
80
- // Setup HomeKit camera controller
81
- if (this.controller === undefined) {
82
- this.controller = new this.hap.CameraController(this.generateControllerOptions());
80
+ // Setup HomeKit camera/doorbell controller
81
+ if (this.controller === undefined && typeof hapController === 'function') {
82
+ // Need to cleanup the CameraOperatingMode service. This is to allow seamless configuration
83
+ // switching between enabling hksv or not
84
+ // Thanks to @bcullman (Brad Ullman) for catching this
85
+ this.accessory.removeService(this.accessory.getService(this.hap.Service.CameraOperatingMode));
86
+ this.controller = new hapController(this.generateControllerOptions());
83
87
  this.accessory.configureController(this.controller);
84
88
  }
85
89
 
@@ -87,7 +91,7 @@ export default class NestCamera extends HomeKitDevice {
87
91
  this.operatingModeService = this.controller?.recordingManagement?.operatingModeService;
88
92
  if (this.operatingModeService === undefined) {
89
93
  // Add in operating mode service for a non-hksv camera/doorbell
90
- // Allow us to change things such as night vision, camera indicator etc within HomeKit for those also:-)
94
+ // Allow us to change things such as night vision, camera indicator etc within HomeKit for those also :-)
91
95
  this.operatingModeService = this.accessory.getService(this.hap.Service.CameraOperatingMode);
92
96
  if (this.operatingModeService === undefined) {
93
97
  this.operatingModeService = this.accessory.addService(this.hap.Service.CameraOperatingMode, '', 1);
@@ -414,7 +418,7 @@ export default class NestCamera extends HomeKitDevice {
414
418
  );
415
419
  let ffmpegRecording = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
416
420
  env: process.env,
417
- stdio: ['pipe', 'pipe', 'pipe', includeAudio === true ? 'pipe' : ''],
421
+ stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
418
422
  });
419
423
 
420
424
  // Process FFmpeg output and parse out the fMP4 stream it's generating for HomeKit Secure Video.
@@ -806,7 +810,7 @@ export default class NestCamera extends HomeKitDevice {
806
810
  );
807
811
  let ffmpegStreaming = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
808
812
  env: process.env,
809
- stdio: ['pipe', 'pipe', 'pipe', includeAudio === true ? 'pipe' : ''],
813
+ stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
810
814
  });
811
815
 
812
816
  ffmpegStreaming.on('exit', (code, signal) => {
package/dist/doorbell.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Nest Doorbell(s)
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 11/10/2024
4
+ // Code version 2025/03/19
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
@@ -21,13 +21,9 @@ export default class NestDoorbell extends NestCamera {
21
21
 
22
22
  // Class functions
23
23
  addServices() {
24
- // Setup some details around the doorbell BEFORE will call out parent addServices function
25
- this.createCameraMotionServices();
26
- this.controller = new this.hap.DoorbellController(this.generateControllerOptions());
27
- this.accessory.configureController(this.controller);
28
-
29
24
  // Call parent to setup the common camera things. Once we return, we can add in the specifics for our doorbell
30
- let postSetupDetails = super.addServices();
25
+ // We pass in the HAP Doorbell controller constructor function here also
26
+ let postSetupDetails = super.addServices(this.hap.DoorbellController);
31
27
 
32
28
  this.switchService = this.accessory.getService(this.hap.Service.Switch);
33
29
  if (this.deviceData.has_indoor_chime === true && this.deviceData.chimeSwitch === true) {
package/dist/protect.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Nest Protect
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 5/10/2024
4
+ // Code version 2025/01/17
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
@@ -74,7 +74,7 @@ export default class NestProtect extends HomeKitDevice {
74
74
  EveSmoke_alarmtest: this.deviceData.self_test_in_progress,
75
75
  EveSmoke_heatstatus: this.deviceData.heat_status,
76
76
  EveSmoke_hushedstate: this.deviceData.hushed_state,
77
- EveSmoke_statusled: this.deviceData.ntp_green_led_enable,
77
+ Evesmoke_statusled: this.deviceData.ntp_green_led_enable,
78
78
  EveSmoke_smoketestpassed: this.deviceData.smoke_test_passed,
79
79
  EveSmoke_heattestpassed: this.deviceData.heat_test_passed,
80
80
  });
@@ -103,50 +103,59 @@ export default class NestProtect extends HomeKitDevice {
103
103
  );
104
104
 
105
105
  // Update smoke details
106
- // If protect isn't online, removed from base, replacement date past, report in HomeKit
106
+ // If protect isn't online, replacement date past, report in HomeKit
107
107
  this.smokeService.updateCharacteristic(
108
108
  this.hap.Characteristic.StatusActive,
109
- deviceData.online === true && deviceData.removed_from_base === false && Math.floor(Date.now() / 1000) <= deviceData.replacement_date,
109
+ deviceData.online === true && Math.floor(Date.now() / 1000) <= deviceData.replacement_date,
110
110
  );
111
111
 
112
112
  this.smokeService.updateCharacteristic(
113
113
  this.hap.Characteristic.StatusFault,
114
- deviceData.online === true && deviceData.removed_from_base === false && Math.floor(Date.now() / 1000) <= deviceData.replacement_date
114
+ deviceData.online === true && Math.floor(Date.now() / 1000) <= deviceData.replacement_date
115
115
  ? this.hap.Characteristic.StatusFault.NO_FAULT
116
116
  : this.hap.Characteristic.StatusFault.GENERAL_FAULT,
117
117
  );
118
118
 
119
119
  this.smokeService.updateCharacteristic(
120
120
  this.hap.Characteristic.SmokeDetected,
121
- deviceData.smoke_status === 2
121
+ deviceData.smoke_status === true
122
122
  ? this.hap.Characteristic.SmokeDetected.SMOKE_DETECTED
123
123
  : this.hap.Characteristic.SmokeDetected.SMOKE_NOT_DETECTED,
124
124
  );
125
125
 
126
- if (deviceData.smoke_status !== 0 && this.deviceData.smoke_status === 0) {
126
+ if (deviceData.smoke_status === true && this.deviceData.smoke_status === false) {
127
127
  this?.log?.warn && this.log.warn('Smoke detected in "%s"', deviceData.description);
128
128
  }
129
129
 
130
- if (deviceData.smoke_status === 0 && this.deviceData.smoke_status !== 0) {
130
+ if (deviceData.smoke_status === false && this.deviceData.smoke_status === true) {
131
131
  this?.log?.info && this.log.info('Smoke is nolonger detected in "%s"', deviceData.description);
132
132
  }
133
133
 
134
134
  // Update carbon monoxide details
135
135
  this.carbonMonoxideService.updateCharacteristic(
136
136
  this.hap.Characteristic.CarbonMonoxideDetected,
137
- deviceData.co_status !== 0
137
+ deviceData.co_status === true
138
138
  ? this.hap.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL
139
139
  : this.hap.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL,
140
140
  );
141
141
 
142
- if (deviceData.co_status !== 0 && this.deviceData.co_status === 0) {
142
+ if (deviceData.co_status === true && this.deviceData.co_status === false) {
143
143
  this?.log?.warn && this.log.warn('Abnormal carbon monoxide levels detected in "%s"', deviceData.description);
144
144
  }
145
145
 
146
- if (deviceData.co_status === 0 && this.deviceData.co_status !== 0) {
146
+ if (deviceData.co_status === false && this.deviceData.co_status === true) {
147
147
  this?.log?.info && this.log.info('Carbon monoxide levels have returned to normal in "%s"', deviceData.description);
148
148
  }
149
149
 
150
+ // Update self testing details
151
+ if (deviceData.self_test_in_progress === true && this.deviceData.self_test_in_progress === false) {
152
+ this?.log?.warn && this.log.info('Smoke and Carbon monoxide sensor testing has started in "%s"', deviceData.description);
153
+ }
154
+
155
+ if (deviceData.self_test_in_progress === false && this.deviceData.self_test_in_progress === true) {
156
+ this?.log?.info && this.log.info('Smoke and Carbon monoxide sensor testing completed in "%s"', deviceData.description);
157
+ }
158
+
150
159
  // Update motion service if present
151
160
  if (this.motionService !== undefined) {
152
161
  this.motionService.updateCharacteristic(this.hap.Characteristic.MotionDetected, deviceData.detected_motion === true);
@@ -3,11 +3,11 @@ syntax = "proto3";
3
3
  import "google/trait/product/camera.proto";
4
4
  import "nest/trait/audio.proto";
5
5
  import "nest/trait/detector.proto";
6
- import "nest/trait/hvac.proto";
7
6
  import "nest/trait/humanlibrary.proto";
8
7
  import "nest/trait/history.proto";
9
- import "nest/trait/humanlibrary.proto";
8
+ import "nest/trait/hvac.proto";
10
9
  import "nest/trait/input.proto";
10
+ import "nest/trait/lighting.proto";
11
11
  import "nest/trait/located.proto";
12
12
  import "nest/trait/media.proto";
13
13
  import "nest/trait/network.proto";
@@ -22,6 +22,7 @@ import "nest/trait/sensor.proto";
22
22
  import "nest/trait/service.proto";
23
23
  import "nest/trait/structure.proto";
24
24
  import "nest/trait/ui.proto";
25
+ import "nest/trait/user.proto";
25
26
  import "weave/common.proto";
26
27
  import "weave/trait/actuator.proto";
27
28
  import "weave/trait/audio.proto";
package/dist/streamer.js CHANGED
@@ -17,7 +17,7 @@
17
17
  //
18
18
  // blankAudio - Buffer containing a blank audio segment for the type of audio being used
19
19
  //
20
- // Code version 20/11/2024
20
+ // Code version 2025/03/16
21
21
  // Mark Hulskamp
22
22
  'use strict';
23
23
 
@@ -162,7 +162,7 @@ export default class Streamer {
162
162
  startBuffering() {
163
163
  if (this.#outputs?.buffer === undefined) {
164
164
  // No active buffer session, start connection to streamer
165
- if (this.connected === undefined && typeof this.connect === 'function') {
165
+ if (this.online === true && this.videoEnabled === true && this.connected === undefined && typeof this.connect === 'function') {
166
166
  this?.log?.debug && this.log.debug('Started buffering for uuid "%s"', this.uuid);
167
167
  this.connect();
168
168
  }
@@ -209,7 +209,7 @@ export default class Streamer {
209
209
  });
210
210
  }
211
211
 
212
- if (this.connected === undefined && typeof this.connect === 'function') {
212
+ if (this.online === true && this.videoEnabled === true && this.connected === undefined && typeof this.connect === 'function') {
213
213
  // We do not have an active connection, so startup connection
214
214
  this.connect();
215
215
  }
@@ -247,7 +247,7 @@ export default class Streamer {
247
247
  });
248
248
  }
249
249
 
250
- if (this.connected === undefined && typeof this.connect === 'function') {
250
+ if (this.connected === undefined && typeof this.connect === 'function' && this.online === true && this.videoEnabled === true) {
251
251
  // We do not have an active connection, so startup connection
252
252
  this.connect();
253
253
  }
@@ -328,7 +328,7 @@ export default class Streamer {
328
328
  if (typeof this.close === 'function') {
329
329
  this.close();
330
330
  }
331
- if (typeof this.connect === 'function') {
331
+ if (this.online === true && this.videoEnabled === true && this.connected === undefined && typeof this.connect === 'function') {
332
332
  this.connect();
333
333
  }
334
334
  }
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 2024/12/01
4
+ // Code version 2025/03/20
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
@@ -129,6 +129,10 @@ export default class NestAccfactory {
129
129
  this.config.options.weather = this.config.options?.weather === true;
130
130
  this.config.options.hksv = this.config.options?.hksv === true;
131
131
 
132
+ // Controls what APIs we use, default is to use both REST and protobuf APIs
133
+ this.config.options.restAPI = this.config.options?.restAPI === true || this.config.options?.restAPI === undefined;
134
+ this.config.options.protobufAPI = this.config.options?.protobufAPI === true || this.config.options?.protobufAPI === undefined;
135
+
132
136
  // Get configuration for max number of concurrent 'live view' streams. For HomeKit Secure Video, this will always be 1
133
137
  this.config.options.maxStreams =
134
138
  isNaN(this.config.options?.maxStreams) === false && this.deviceData?.hksv === false
@@ -137,16 +141,13 @@ export default class NestAccfactory {
137
141
  ? 1
138
142
  : 2;
139
143
 
140
- // Check if a ffmpeg binary exists in current path OR the specific path via configuration
141
- // If using HomeBridge, the default path will be where the Homebridge user folder is, otherwise the current directory
144
+ // Check if a ffmpeg binary exist via a specific path in configuration OR /usr/local/bin
142
145
  this.config.options.ffmpeg = {};
143
146
  this.config.options.ffmpeg.debug = this.config.options?.ffmpegDebug === true;
144
147
  this.config.options.ffmpeg.binary = path.resolve(
145
148
  typeof this.config.options?.ffmpegPath === 'string' && this.config.options.ffmpegPath !== ''
146
149
  ? this.config.options.ffmpegPath
147
- : typeof api?.user?.storagePath === 'function'
148
- ? api.user.storagePath()
149
- : __dirname,
150
+ : '/usr/local/bin',
150
151
  );
151
152
 
152
153
  // If the path doesn't include 'ffmpeg' on the end, we'll add it here
@@ -170,6 +171,7 @@ export default class NestAccfactory {
170
171
  this.config.options.ffmpeg.binary = undefined;
171
172
  }
172
173
 
174
+ // Process ffmpeg binary to see if we can use it
173
175
  if (fs.existsSync(this.config.options.ffmpeg.binary) === true) {
174
176
  let ffmpegProcess = child_process.spawnSync(this.config.options.ffmpeg.binary, ['-version'], {
175
177
  env: process.env,
@@ -186,7 +188,11 @@ export default class NestAccfactory {
186
188
  this.config.options.ffmpeg.libx264 = ffmpegProcess.stdout.toString().includes('--enable-libx264') === true;
187
189
  this.config.options.ffmpeg.libfdk_aac = ffmpegProcess.stdout.toString().includes('--enable-libfdk-aac') === true;
188
190
  if (
189
- this.config.options.ffmpeg.version.replace(/\./gi, '') < parseFloat(FFMPEGVERSION.toString().replace(/\./gi, '')) ||
191
+ this.config.options.ffmpeg.version.localeCompare(FFMPEGVERSION, undefined, {
192
+ numeric: true,
193
+ sensitivity: 'case',
194
+ caseFirst: 'upper',
195
+ }) === -1 ||
190
196
  this.config.options.ffmpeg.libspeex === false ||
191
197
  this.config.options.ffmpeg.libopus === false ||
192
198
  this.config.options.ffmpeg.libx264 === false ||
@@ -194,7 +200,13 @@ export default class NestAccfactory {
194
200
  ) {
195
201
  this?.log?.warn &&
196
202
  this.log.warn('ffmpeg binary "%s" does not meet the minimum support requirements', this.config.options.ffmpeg.binary);
197
- if (this.config.options.ffmpeg.version.replace(/\./gi, '') < parseFloat(FFMPEGVERSION.toString().replace(/\./gi, ''))) {
203
+ if (
204
+ this.config.options.ffmpeg.version.localeCompare(FFMPEGVERSION, undefined, {
205
+ numeric: true,
206
+ sensitivity: 'case',
207
+ caseFirst: 'upper',
208
+ }) === -1
209
+ ) {
198
210
  this?.log?.warn &&
199
211
  this.log.warn(
200
212
  'Minimum binary version is "%s", however the installed version is "%s"',
@@ -243,6 +255,10 @@ export default class NestAccfactory {
243
255
  }
244
256
  }
245
257
 
258
+ if (this.config.options.ffmpeg.binary !== undefined) {
259
+ this?.log?.success && this.log.success('Found valid ffmpeg binary in %s', this.config.options.ffmpeg.binary);
260
+ }
261
+
246
262
  if (this.api instanceof EventEmitter === true) {
247
263
  this.api.on('didFinishLaunching', async () => {
248
264
  // We got notified that Homebridge has finished loading, so we are ready to process
@@ -287,7 +303,7 @@ export default class NestAccfactory {
287
303
  }
288
304
 
289
305
  configureAccessory(accessory) {
290
- // This gets called from HomeBridge each time it restores an accessory from its cache
306
+ // This gets called from Homebridge each time it restores an accessory from its cache
291
307
  this?.log?.info && this.log.info('Loading accessory from cache:', accessory.displayName);
292
308
 
293
309
  // add the restored accessory to the accessories cache, so we can track if it has already been registered
@@ -298,8 +314,10 @@ export default class NestAccfactory {
298
314
  Object.keys(this.#connections).forEach((uuid) => {
299
315
  if (this.#connections[uuid].authorised === false) {
300
316
  this.#connect(uuid).then(() => {
301
- if (this.#connections[uuid].authorised === true) {
317
+ if (this.#connections[uuid].authorised === true && this.config.options?.restAPI === true) {
302
318
  this.#subscribeREST(uuid, true);
319
+ }
320
+ if (this.#connections[uuid].authorised === true && this.config.options?.protobufAPI === true) {
303
321
  this.#subscribeProtobuf(uuid, true);
304
322
  }
305
323
  });
@@ -701,11 +719,13 @@ export default class NestAccfactory {
701
719
  });
702
720
 
703
721
  // Send removed notice onto HomeKit device for it to process
704
- this.#eventEmitter.emit(
705
- this.#trackedDevices[this.#rawData[object_key].value.serial_number].uuid,
706
- HomeKitDevice.REMOVE,
707
- {},
708
- );
722
+ if (this.#eventEmitter !== undefined) {
723
+ this.#eventEmitter.emit(
724
+ this.#trackedDevices[this.#rawData[object_key].value.serial_number].uuid,
725
+ HomeKitDevice.REMOVE,
726
+ {},
727
+ );
728
+ }
709
729
 
710
730
  // Finally, remove from tracked devices
711
731
  delete this.#trackedDevices[this.#rawData[object_key].value.serial_number];
@@ -909,11 +929,13 @@ export default class NestAccfactory {
909
929
  }
910
930
 
911
931
  // Send removed notice onto HomeKit device for it to process
912
- this.#eventEmitter.emit(
913
- this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber].uuid,
914
- HomeKitDevice.REMOVE,
915
- {},
916
- );
932
+ if (this.#eventEmitter !== undefined) {
933
+ this.#eventEmitter.emit(
934
+ this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber].uuid,
935
+ HomeKitDevice.REMOVE,
936
+ {},
937
+ );
938
+ }
917
939
 
918
940
  // Finally, remove from tracked devices
919
941
  delete this.#trackedDevices[this.#rawData[resource.resourceId].value.device_identity.serialNumber];
@@ -972,6 +994,7 @@ export default class NestAccfactory {
972
994
  this.log.debug('Error was "%s"', error);
973
995
  }
974
996
  })
997
+
975
998
  .finally(() => {
976
999
  this?.log?.debug && this.log.debug('Restarting Protobuf API trait observe for connection uuid "%s"', connectionUUID);
977
1000
  setTimeout(this.#subscribeProtobuf.bind(this, connectionUUID, false), 1000);
@@ -983,7 +1006,16 @@ export default class NestAccfactory {
983
1006
  if (this.#trackedDevices?.[deviceData?.serialNumber] === undefined && deviceData?.excluded === true) {
984
1007
  // We haven't tracked this device before (ie: should be a new one) and but its excluded
985
1008
  this?.log?.warn && this.log.warn('Device "%s" is ignored due to it being marked as excluded', deviceData.description);
1009
+
1010
+ // Track this device even though its excluded
1011
+ this.#trackedDevices[deviceData.serialNumber] = {
1012
+ uuid: undefined,
1013
+ rawDataUuid: deviceData.nest_google_uuid,
1014
+ source: undefined,
1015
+ exclude: true,
1016
+ };
986
1017
  }
1018
+
987
1019
  if (this.#trackedDevices?.[deviceData?.serialNumber] === undefined && deviceData?.excluded === false) {
988
1020
  // We haven't tracked this device before (ie: should be a new one) and its not excluded
989
1021
  // so create the required HomeKit accessories based upon the device data
@@ -1097,7 +1129,7 @@ export default class NestAccfactory {
1097
1129
  this.#rawData[nest_google_uuid].value.activity_zones = zones;
1098
1130
 
1099
1131
  // Send updated data onto HomeKit device for it to process
1100
- if (this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
1132
+ if (this.#eventEmitter !== undefined && this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
1101
1133
  this.#eventEmitter.emit(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, {
1102
1134
  activity_zones: zones,
1103
1135
  });
@@ -1258,7 +1290,7 @@ export default class NestAccfactory {
1258
1290
  this.#rawData[nest_google_uuid].value.alerts = alerts;
1259
1291
 
1260
1292
  // Send updated alerts onto HomeKit device for it to process
1261
- if (this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
1293
+ if (this.#eventEmitter !== undefined && this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
1262
1294
  this.#eventEmitter.emit(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, {
1263
1295
  alerts: alerts,
1264
1296
  });
@@ -1292,7 +1324,7 @@ export default class NestAccfactory {
1292
1324
  );
1293
1325
 
1294
1326
  // Send updated weather data onto HomeKit device for it to process
1295
- if (this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
1327
+ if (this.#eventEmitter !== undefined && this.#trackedDevices?.[deviceData?.serialNumber]?.uuid !== undefined) {
1296
1328
  this.#eventEmitter.emit(
1297
1329
  this.#trackedDevices[deviceData.serialNumber].uuid,
1298
1330
  HomeKitDevice.UPDATE,
@@ -1323,7 +1355,9 @@ export default class NestAccfactory {
1323
1355
  this.#trackedDevices[deviceData.serialNumber].rawDataUuid = deviceData.nest_google_uuid;
1324
1356
  }
1325
1357
 
1326
- this.#eventEmitter.emit(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, deviceData);
1358
+ if (this.#eventEmitter !== undefined) {
1359
+ this.#eventEmitter.emit(this.#trackedDevices[deviceData.serialNumber].uuid, HomeKitDevice.UPDATE, deviceData);
1360
+ }
1327
1361
  }
1328
1362
  });
1329
1363
  }
@@ -1458,7 +1492,11 @@ export default class NestAccfactory {
1458
1492
  .forEach(([object_key, value]) => {
1459
1493
  let tempDevice = {};
1460
1494
  try {
1461
- if (value?.source === NestAccfactory.DataSource.PROTOBUF && value.value?.configuration_done?.deviceReady === true) {
1495
+ if (
1496
+ value?.source === NestAccfactory.DataSource.PROTOBUF &&
1497
+ this.config.options?.protobufAPI === true &&
1498
+ value.value?.configuration_done?.deviceReady === true
1499
+ ) {
1462
1500
  let RESTTypeData = {};
1463
1501
  RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
1464
1502
  RESTTypeData.softwareVersion =
@@ -1688,7 +1726,11 @@ export default class NestAccfactory {
1688
1726
  tempDevice = process_thermostat_data(object_key, RESTTypeData);
1689
1727
  }
1690
1728
 
1691
- if (value?.source === NestAccfactory.DataSource.REST && value.value?.where_id !== undefined) {
1729
+ if (
1730
+ value?.source === NestAccfactory.DataSource.REST &&
1731
+ this.config.options?.restAPI === true &&
1732
+ value.value?.where_id !== undefined
1733
+ ) {
1692
1734
  let RESTTypeData = {};
1693
1735
  RESTTypeData.serialNumber = value.value.serial_number;
1694
1736
  RESTTypeData.softwareVersion = value.value.current_version;
@@ -1953,6 +1995,7 @@ export default class NestAccfactory {
1953
1995
  try {
1954
1996
  if (
1955
1997
  value?.source === NestAccfactory.DataSource.PROTOBUF &&
1998
+ this.config.options?.protobufAPI === true &&
1956
1999
  value.value?.configuration_done?.deviceReady === true &&
1957
2000
  typeof value?.value?.associated_thermostat === 'string' &&
1958
2001
  value?.value?.associated_thermostat !== ''
@@ -1977,6 +2020,7 @@ export default class NestAccfactory {
1977
2020
  }
1978
2021
  if (
1979
2022
  value?.source === NestAccfactory.DataSource.REST &&
2023
+ this.config.options?.restAPI === true &&
1980
2024
  value.value?.where_id !== undefined &&
1981
2025
  value.value?.structure_id !== undefined &&
1982
2026
  typeof value?.value?.associated_thermostat === 'string' &&
@@ -2014,7 +2058,6 @@ export default class NestAccfactory {
2014
2058
  // Fix up data we need to
2015
2059
  data = process_common_data(object_key, data);
2016
2060
  data.device_type = NestAccfactory.DeviceType.SMOKESENSOR; // Nest Protect
2017
- data.battery_level = scaleValue(data.battery_level, 0, 5400, 0, 100);
2018
2061
  data.model = 'Protect';
2019
2062
  if (data.wired_or_battery === 0) {
2020
2063
  data.model = data.model + ' (wired'; // Mains powered
@@ -2046,8 +2089,68 @@ export default class NestAccfactory {
2046
2089
  .forEach(([object_key, value]) => {
2047
2090
  let tempDevice = {};
2048
2091
  try {
2092
+ if (
2093
+ value?.source === NestAccfactory.DataSource.PROTOBUF &&
2094
+ this.config.options?.protobufAPI === true &&
2095
+ value.value?.configuration_done?.deviceReady === true
2096
+ ) {
2097
+ let RESTTypeData = {};
2098
+ RESTTypeData.serialNumber = value.value.device_identity.serialNumber;
2099
+ RESTTypeData.softwareVersion =
2100
+ value.value.device_identity.softwareVersion.split(/\s+/)?.[3] !== undefined
2101
+ ? value.value.device_identity.softwareVersion.split(/\s+/)?.[3]
2102
+ : value.value.device_identity.softwareVersion;
2103
+ RESTTypeData.online = value.value?.liveness?.status === 'LIVENESS_DEVICE_STATUS_ONLINE';
2104
+ RESTTypeData.line_power_present = value.value?.wall_power?.status === 'POWER_SOURCE_STATUS_ACTIVE';
2105
+ RESTTypeData.wired_or_battery = typeof value.value?.wall_power?.status === 'string' ? 0 : 1;
2106
+ RESTTypeData.battery_level =
2107
+ isNaN(value.value?.battery_voltage_bank1?.batteryValue?.batteryVoltage?.value) === false
2108
+ ? scaleValue(Number(value.value.battery_voltage_bank1.batteryValue.batteryVoltage.value), 0, 5.4, 0, 100)
2109
+ : 0;
2110
+ RESTTypeData.battery_health_state =
2111
+ value.value?.battery_voltage_bank0?.faultInformation === undefined &&
2112
+ value.value?.battery_voltage_bank1?.faultInformation === undefined
2113
+ ? 0
2114
+ : 1;
2115
+ RESTTypeData.smoke_status = value.value?.safety_alarm_smoke?.alarmState === 'ALARM_STATE_ALARM';
2116
+ RESTTypeData.co_status = value.value?.safety_alarm_co?.alarmState === 'ALARM_STATE_ALARM';
2117
+ RESTTypeData.heat_status = false; // To find in protobuf
2118
+ RESTTypeData.hushed_state =
2119
+ value.value?.safety_alarm_smoke?.silenceState === 'SILENCE_STATE_SILENCED' ||
2120
+ value.value?.safety_alarm_co?.silenceState === 'SILENCE_STATE_SILENCED';
2121
+ RESTTypeData.ntp_green_led = value.value?.night_time_promise_settings?.greenLedEnabled === true;
2122
+ RESTTypeData.smoke_test_passed =
2123
+ typeof value.value.safety_summary?.warningDevices?.failures === 'object'
2124
+ ? value.value.safety_summary?.warningDevices?.failures.includes('FAILURE_TYPE_SMOKE') === false
2125
+ : true;
2126
+ RESTTypeData.heat_test_passed =
2127
+ typeof value.value.safety_summary?.warningDevices?.failures === 'object'
2128
+ ? value.value.safety_summary?.warningDevices?.failures.includes('FAILURE_TYPE_TEMP') === false
2129
+ : true;
2130
+ RESTTypeData.latest_alarm_test =
2131
+ isNaN(value.value.self_test?.lastMstEnd?.seconds) === false ? Number(value.value.self_test.lastMstEnd.seconds) : 0;
2132
+ RESTTypeData.self_test_in_progress =
2133
+ value.value?.legacy_structure_self_test?.mstInProgress === true ||
2134
+ value.value?.legacy_structure_self_test?.astInProgress === true;
2135
+ RESTTypeData.replacement_date =
2136
+ isNaN(value.value.legacy_protect_device_settings?.replaceByDate?.seconds) === false
2137
+ ? Number(value.value.legacy_protect_device_settings.replaceByDate.seconds)
2138
+ : 0;
2139
+ RESTTypeData.topaz_hush_key =
2140
+ typeof value.value?.safety_structure_settings?.structureHushKey === 'string'
2141
+ ? value.value.safety_structure_settings.structureHushKey
2142
+ : '';
2143
+ RESTTypeData.detected_motion = value.value?.legacy_protect_device_info?.autoAway !== true; // undefined or false = motion
2144
+ RESTTypeData.description = typeof value.value?.label?.label === 'string' ? value.value.label.label : '';
2145
+ RESTTypeData.location = get_location_name(
2146
+ value.value?.device_info?.pairerId?.resourceId,
2147
+ value.value?.device_located_settings?.whereAnnotationRid?.resourceId,
2148
+ );
2149
+ tempDevice = process_protect_data(object_key, RESTTypeData);
2150
+ }
2049
2151
  if (
2050
2152
  value?.source === NestAccfactory.DataSource.REST &&
2153
+ this.config.options?.restAPI === true &&
2051
2154
  value.value?.where_id !== undefined &&
2052
2155
  value.value?.structure_id !== undefined
2053
2156
  ) {
@@ -2060,11 +2163,11 @@ export default class NestAccfactory {
2060
2163
  : false;
2061
2164
  RESTTypeData.line_power_present = value.value.line_power_present === true;
2062
2165
  RESTTypeData.wired_or_battery = value.value.wired_or_battery;
2063
- RESTTypeData.battery_level = value.value.battery_level;
2166
+ RESTTypeData.battery_level = scaleValue(value.value.battery_level, 0, 5400, 0, 100);
2064
2167
  RESTTypeData.battery_health_state = value.value.battery_health_state;
2065
- RESTTypeData.smoke_status = value.value.smoke_status;
2066
- RESTTypeData.co_status = value.value.co_status;
2067
- RESTTypeData.heat_status = value.value.heat_status;
2168
+ RESTTypeData.smoke_status = value.value.smoke_status !== 0;
2169
+ RESTTypeData.co_status = value.value.co_status !== 0;
2170
+ RESTTypeData.heat_status = value.value.heat_status !== 0;
2068
2171
  RESTTypeData.hushed_state = value.value.hushed_state === true;
2069
2172
  RESTTypeData.ntp_green_led_enable = value.value.ntp_green_led_enable === true;
2070
2173
  RESTTypeData.smoke_test_passed = value.value.component_smoke_test_passed === true;
@@ -2073,7 +2176,6 @@ export default class NestAccfactory {
2073
2176
  RESTTypeData.self_test_in_progress =
2074
2177
  this.#rawData?.['safety.' + value.value.structure_id]?.value?.manual_self_test_in_progress === true;
2075
2178
  RESTTypeData.replacement_date = value.value.replace_by_date_utc_secs;
2076
- RESTTypeData.removed_from_base = value.value.removed_from_base === true;
2077
2179
  RESTTypeData.topaz_hush_key =
2078
2180
  typeof this.#rawData?.['structure.' + value.value.structure_id]?.value?.topaz_hush_key === 'string'
2079
2181
  ? this.#rawData?.['structure.' + value.value.structure_id]?.value?.topaz_hush_key
@@ -2141,6 +2243,7 @@ export default class NestAccfactory {
2141
2243
  try {
2142
2244
  if (
2143
2245
  value?.source === NestAccfactory.DataSource.PROTOBUF &&
2246
+ this.config.options?.protobufAPI === true &&
2144
2247
  Array.isArray(value.value?.streaming_protocol?.supportedProtocols) === true &&
2145
2248
  value.value.streaming_protocol.supportedProtocols.includes('PROTOCOL_WEBRTC') === true &&
2146
2249
  (value.value?.configuration_done?.deviceReady === true ||
@@ -2255,6 +2358,7 @@ export default class NestAccfactory {
2255
2358
 
2256
2359
  if (
2257
2360
  value?.source === NestAccfactory.DataSource.REST &&
2361
+ this.config.options?.restAPI === true &&
2258
2362
  value.value?.where_id !== undefined &&
2259
2363
  value.value?.structure_id !== undefined &&
2260
2364
  value.value?.nexus_api_http_server_url !== undefined &&
@@ -2388,6 +2492,7 @@ export default class NestAccfactory {
2388
2492
  try {
2389
2493
  if (
2390
2494
  value?.source === NestAccfactory.DataSource.PROTOBUF &&
2495
+ this.config.options?.protobufAPI === true &&
2391
2496
  value.value?.structure_location?.geoCoordinate?.latitude !== undefined &&
2392
2497
  value.value?.structure_location?.geoCoordinate?.longitude !== undefined
2393
2498
  ) {
@@ -2412,6 +2517,7 @@ export default class NestAccfactory {
2412
2517
 
2413
2518
  if (
2414
2519
  value?.source === NestAccfactory.DataSource.REST &&
2520
+ this.config.options?.restAPI === true &&
2415
2521
  value.value?.latitude !== undefined &&
2416
2522
  value.value?.longitude !== undefined
2417
2523
  ) {
@@ -3130,7 +3236,7 @@ function makeHomeKitName(nameToMakeValid) {
3130
3236
  // Matches against uni-code characters
3131
3237
  return typeof nameToMakeValid === 'string'
3132
3238
  ? nameToMakeValid
3133
- .replace(/[^\p{L}\p{N}\p{Z}\u2019.,-]/gu, '')
3239
+ .replace(/[^\p{L}\p{N}\p{Z}\u2019 '.,-]/gu, '')
3134
3240
  .replace(/^[^\p{L}\p{N}]*/gu, '')
3135
3241
  .replace(/[^\p{L}\p{N}]+$/gu, '')
3136
3242
  : nameToMakeValid;
package/dist/webrtc.js CHANGED
@@ -2,14 +2,15 @@
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
4
  // Handles connection and data from Google WebRTC systems
5
+ // Currently a "work in progress"
5
6
  //
6
- // Code version 15/11/2024
7
+ // Code version 2025/03/16
7
8
  // Mark Hulskamp
8
9
  'use strict';
9
10
 
10
11
  // Define external library requirements
11
12
  import protobuf from 'protobufjs';
12
- import werift from 'werift';
13
+ import * as werift from 'werift';
13
14
 
14
15
  // Define nodejs module requirements
15
16
  import EventEmitter from 'node:events';
@@ -98,194 +99,196 @@ export default class WebRTC extends Streamer {
98
99
  this.stalledTimer = undefined;
99
100
  this.#id = undefined;
100
101
 
101
- if (this.#googleHomeDeviceUUID === undefined) {
102
- // We don't have the 'google id' yet for this device, so obtain
103
- let homeFoyerResponse = await this.#googleHomeFoyerCommand('StructuresService', 'GetHomeGraph', {
104
- requestId: crypto.randomUUID(),
105
- });
106
-
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 :-)
109
- if (homeFoyerResponse?.data?.[0]?.homes !== undefined) {
110
- Object.values(homeFoyerResponse?.data?.[0]?.homes).forEach((home) => {
111
- Object.values(home.devices).forEach((device) => {
112
- if (device?.id?.googleUuid !== undefined && device?.otherIds?.otherThirdPartyId !== undefined) {
113
- // Test to see if our uuid matches here
114
- let currentGoogleUuid = device?.id?.googleUuid;
115
- Object.values(device.otherIds.otherThirdPartyId).forEach((other) => {
116
- if (other?.id === this.uuid) {
117
- this.#googleHomeDeviceUUID = currentGoogleUuid;
118
- }
119
- });
120
- }
121
- });
102
+ if (this.online === true && this.videoEnabled === true) {
103
+ if (this.#googleHomeDeviceUUID === undefined) {
104
+ // We don't have the 'google id' yet for this device, so obtain
105
+ let homeFoyerResponse = await this.#googleHomeFoyerCommand('StructuresService', 'GetHomeGraph', {
106
+ requestId: crypto.randomUUID(),
122
107
  });
123
- }
124
- }
125
-
126
- if (this.#googleHomeDeviceUUID !== undefined) {
127
- // Start setting up connection to camera stream
128
- this.connected = false; // Starting connection
129
- let homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'SendCameraViewIntent', {
130
- request: {
131
- googleDeviceId: {
132
- value: this.#googleHomeDeviceUUID,
133
- },
134
- command: 'VIEW_INTENT_START',
135
- },
136
- });
137
108
 
138
- if (homeFoyerResponse.status !== 0) {
139
- this.connected = undefined;
140
- this?.log?.debug && this.log.debug('Request to start camera viewing was not accepted for uuid "%s"', this.uuid);
109
+ // Translate our uuid (DEVICE_xxxxxxxxxx) into the associated 'google id' from the Google Home Foyer
110
+ // We need this id for SOME calls to Google Home Foyer services. Gotta love consistancy :-)
111
+ if (homeFoyerResponse?.data?.[0]?.homes !== undefined) {
112
+ Object.values(homeFoyerResponse?.data?.[0]?.homes).forEach((home) => {
113
+ Object.values(home.devices).forEach((device) => {
114
+ if (device?.id?.googleUuid !== undefined && device?.otherIds?.otherThirdPartyId !== undefined) {
115
+ // Test to see if our uuid matches here
116
+ let currentGoogleUuid = device?.id?.googleUuid;
117
+ Object.values(device.otherIds.otherThirdPartyId).forEach((other) => {
118
+ if (other?.id === this.uuid) {
119
+ this.#googleHomeDeviceUUID = currentGoogleUuid;
120
+ }
121
+ });
122
+ }
123
+ });
124
+ });
125
+ }
141
126
  }
142
127
 
143
- if (homeFoyerResponse.status === 0) {
144
- // Setup our WebWRTC peer connection for this device
145
- this.#peerConnection = new werift.RTCPeerConnection({
146
- iceUseIpv4: true,
147
- iceUseIpv6: false,
148
- bundlePolicy: 'max-bundle',
149
- codecs: {
150
- audio: [
151
- new werift.RTCRtpCodecParameters({
152
- mimeType: 'audio/opus',
153
- clockRate: 48000,
154
- channels: 2,
155
- rtcpFeedback: [{ type: 'transport-cc' }, { type: 'nack' }],
156
- parameters: 'minptime=10;useinbandfec=1',
157
- payloadType: RTP_AUDIO_PAYLOAD_TYPE,
158
- }),
159
- ],
160
- video: [
161
- // H264 Main profile, level 4.0
162
- new werift.RTCRtpCodecParameters({
163
- mimeType: 'video/H264',
164
- clockRate: 90000,
165
- rtcpFeedback: [
166
- { type: 'transport-cc' },
167
- { type: 'ccm', parameter: 'fir' },
168
- { type: 'nack' },
169
- { type: 'nack', parameter: 'pli' },
170
- { type: 'goog-remb' },
171
- ],
172
- parameters: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
173
- payloadType: RTP_VIDEO_PAYLOAD_TYPE,
174
- }),
175
- ],
176
- },
177
- headerExtensions: {
178
- audio: [werift.useTransportWideCC(), werift.useAudioLevelIndication()],
128
+ if (this.#googleHomeDeviceUUID !== undefined) {
129
+ // Start setting up connection to camera stream
130
+ this.connected = false; // Starting connection
131
+ let homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'SendCameraViewIntent', {
132
+ request: {
133
+ googleDeviceId: {
134
+ value: this.#googleHomeDeviceUUID,
135
+ },
136
+ command: 'VIEW_INTENT_START',
179
137
  },
180
138
  });
181
139
 
182
- this.#peerConnection.createDataChannel('webrtc-datachannel');
140
+ if (homeFoyerResponse.status !== 0) {
141
+ this.connected = undefined;
142
+ this?.log?.debug && this.log.debug('Request to start camera viewing was not accepted for uuid "%s"', this.uuid);
143
+ }
183
144
 
184
- this.#audioTransceiver = this.#peerConnection.addTransceiver('audio', {
185
- direction: 'sendrecv',
186
- });
145
+ if (homeFoyerResponse.status === 0) {
146
+ // Setup our WebWRTC peer connection for this device
147
+ this.#peerConnection = new werift.RTCPeerConnection({
148
+ iceUseIpv4: true,
149
+ iceUseIpv6: false,
150
+ bundlePolicy: 'max-bundle',
151
+ codecs: {
152
+ audio: [
153
+ new werift.RTCRtpCodecParameters({
154
+ mimeType: 'audio/opus',
155
+ clockRate: 48000,
156
+ channels: 2,
157
+ rtcpFeedback: [{ type: 'transport-cc' }, { type: 'nack' }],
158
+ parameters: 'minptime=10;useinbandfec=1',
159
+ payloadType: RTP_AUDIO_PAYLOAD_TYPE,
160
+ }),
161
+ ],
162
+ video: [
163
+ // H264 Main profile, level 4.0
164
+ new werift.RTCRtpCodecParameters({
165
+ mimeType: 'video/H264',
166
+ clockRate: 90000,
167
+ rtcpFeedback: [
168
+ { type: 'transport-cc' },
169
+ { type: 'ccm', parameter: 'fir' },
170
+ { type: 'nack' },
171
+ { type: 'nack', parameter: 'pli' },
172
+ { type: 'goog-remb' },
173
+ ],
174
+ parameters: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
175
+ payloadType: RTP_VIDEO_PAYLOAD_TYPE,
176
+ }),
177
+ ],
178
+ },
179
+ headerExtensions: {
180
+ audio: [werift.useTransportWideCC(), werift.useAudioLevelIndication()],
181
+ },
182
+ });
187
183
 
188
- this.#videoTransceiver = this.#peerConnection.addTransceiver('video', {
189
- direction: 'recvonly',
190
- });
184
+ this.#peerConnection.createDataChannel('webrtc-datachannel');
191
185
 
192
- let webRTCOffer = await this.#peerConnection.createOffer();
193
- await this.#peerConnection.setLocalDescription(webRTCOffer);
186
+ this.#audioTransceiver = this.#peerConnection.addTransceiver('audio', {
187
+ direction: 'sendrecv',
188
+ });
194
189
 
195
- this?.log?.debug && this.log.debug('Sending WebRTC offer for uuid "%s"', this.uuid);
190
+ this.#videoTransceiver = this.#peerConnection.addTransceiver('video', {
191
+ direction: 'recvonly',
192
+ });
196
193
 
197
- homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'JoinStream', {
198
- command: 'offer',
199
- deviceId: this.uuid,
200
- local: this.localAccess,
201
- streamContext: 'STREAM_CONTEXT_DEFAULT',
202
- requestedVideoResolution: 'VIDEO_RESOLUTION_FULL_HIGH',
203
- sdp: webRTCOffer.sdp,
204
- });
194
+ let webRTCOffer = await this.#peerConnection.createOffer();
195
+ await this.#peerConnection.setLocalDescription(webRTCOffer);
205
196
 
206
- if (homeFoyerResponse.status !== 0) {
207
- this.connected = undefined;
208
- this?.log?.debug && this.log.debug('WebRTC offer was not agreed with remote for uuid "%s"', this.uuid);
209
- }
197
+ this?.log?.debug && this.log.debug('Sending WebRTC offer for uuid "%s"', this.uuid);
210
198
 
211
- if (
212
- homeFoyerResponse.status === 0 &&
213
- homeFoyerResponse.data?.[0]?.responseType === 'answer' &&
214
- homeFoyerResponse.data?.[0]?.streamId !== undefined
215
- ) {
216
- this?.log?.debug && this.log.debug('WebRTC offer agreed with remote for uuid "%s"', this.uuid);
199
+ homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'JoinStream', {
200
+ command: 'offer',
201
+ deviceId: this.uuid,
202
+ local: this.localAccess,
203
+ streamContext: 'STREAM_CONTEXT_DEFAULT',
204
+ requestedVideoResolution: 'VIDEO_RESOLUTION_FULL_HIGH',
205
+ sdp: webRTCOffer.sdp,
206
+ });
217
207
 
218
- this.#audioTransceiver?.onTrack &&
219
- this.#audioTransceiver.onTrack.subscribe((track) => {
220
- this.#handlePlaybackBegin(track);
208
+ if (homeFoyerResponse.status !== 0) {
209
+ this.connected = undefined;
210
+ this?.log?.debug && this.log.debug('WebRTC offer was not agreed with remote for uuid "%s"', this.uuid);
211
+ }
221
212
 
222
- track.onReceiveRtp.subscribe((rtp) => {
223
- this.#handlePlaybackPacket(rtp);
224
- });
225
- });
213
+ if (
214
+ homeFoyerResponse.status === 0 &&
215
+ homeFoyerResponse.data?.[0]?.responseType === 'answer' &&
216
+ homeFoyerResponse.data?.[0]?.streamId !== undefined
217
+ ) {
218
+ this?.log?.debug && this.log.debug('WebRTC offer agreed with remote for uuid "%s"', this.uuid);
226
219
 
227
- this.#videoTransceiver?.onTrack &&
228
- this.#videoTransceiver.onTrack.subscribe((track) => {
229
- this.#handlePlaybackBegin(track);
220
+ this.#audioTransceiver?.onTrack &&
221
+ this.#audioTransceiver.onTrack.subscribe((track) => {
222
+ this.#handlePlaybackBegin(track);
230
223
 
231
- track.onReceiveRtp.subscribe((rtp) => {
232
- this.#handlePlaybackPacket(rtp);
224
+ track.onReceiveRtp.subscribe((rtp) => {
225
+ this.#handlePlaybackPacket(rtp);
226
+ });
233
227
  });
234
- track.onReceiveRtcp.once(() => {
235
- setInterval(() => {
236
- if (this.#videoTransceiver?.receiver !== undefined) {
237
- this.#videoTransceiver.receiver.sendRtcpPLI(track.ssrc);
238
- }
239
- }, 2000);
228
+
229
+ this.#videoTransceiver?.onTrack &&
230
+ this.#videoTransceiver.onTrack.subscribe((track) => {
231
+ this.#handlePlaybackBegin(track);
232
+
233
+ track.onReceiveRtp.subscribe((rtp) => {
234
+ this.#handlePlaybackPacket(rtp);
235
+ });
236
+ track.onReceiveRtcp.once(() => {
237
+ setInterval(() => {
238
+ if (this.#videoTransceiver?.receiver !== undefined) {
239
+ this.#videoTransceiver.receiver.sendRtcpPLI(track.ssrc);
240
+ }
241
+ }, 2000);
242
+ });
240
243
  });
241
- });
242
244
 
243
- this.#id = homeFoyerResponse.data[0].streamId;
244
- this.#peerConnection &&
245
- (await this.#peerConnection.setRemoteDescription({
246
- type: 'answer',
247
- sdp: homeFoyerResponse.data[0].sdp,
248
- }));
249
-
250
- this?.log?.debug && this.log.debug('Playback started from WebRTC for uuid "%s" with session ID "%s"', this.uuid, this.#id);
251
- this.connected = true;
252
-
253
- // Monitor connection status. If closed and there are still output streams, re-connect
254
- // Never seem to get a 'connected' status. Could use that for something?
255
- this.#peerConnection &&
256
- this.#peerConnection.connectionStateChange.subscribe((state) => {
257
- if (state !== 'connected' && state !== 'connecting') {
258
- this?.log?.debug && this.log.debug('Connection closed to WebRTC for uuid "%s"', this.uuid);
259
- this.connected = undefined;
260
- if (this.haveOutputs() === true) {
261
- this.connect();
245
+ this.#id = homeFoyerResponse.data[0].streamId;
246
+ this.#peerConnection &&
247
+ (await this.#peerConnection.setRemoteDescription({
248
+ type: 'answer',
249
+ sdp: homeFoyerResponse.data[0].sdp,
250
+ }));
251
+
252
+ this?.log?.debug && this.log.debug('Playback started from WebRTC for uuid "%s" with session ID "%s"', this.uuid, this.#id);
253
+ this.connected = true;
254
+
255
+ // Monitor connection status. If closed and there are still output streams, re-connect
256
+ // Never seem to get a 'connected' status. Could use that for something?
257
+ this.#peerConnection &&
258
+ this.#peerConnection.connectionStateChange.subscribe((state) => {
259
+ if (state !== 'connected' && state !== 'connecting') {
260
+ this?.log?.debug && this.log.debug('Connection closed to WebRTC for uuid "%s"', this.uuid);
261
+ this.connected = undefined;
262
+ if (this.haveOutputs() === true) {
263
+ this.connect();
264
+ }
262
265
  }
263
- }
264
- });
265
-
266
- // Create a timer to extend the active stream every period as defined
267
- this.extendTimer = setInterval(async () => {
268
- if (
269
- this.#googleHomeFoyer !== undefined &&
270
- this.connected === true &&
271
- this.#id !== undefined &&
272
- this.#googleHomeDeviceUUID !== undefined
273
- ) {
274
- let homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'JoinStream', {
275
- command: 'extend',
276
- deviceId: this.uuid,
277
- streamId: this.#id,
278
266
  });
279
267
 
280
- if (homeFoyerResponse?.data?.[0]?.streamExtensionStatus !== 'STATUS_STREAM_EXTENDED') {
281
- this?.log?.debug && this.log.debug('Error occurred while requested stream extension for uuid "%s"', this.uuid);
282
-
283
- if (typeof this.#peerConnection?.close === 'function') {
284
- await this.#peerConnection.close();
268
+ // Create a timer to extend the active stream every period as defined
269
+ this.extendTimer = setInterval(async () => {
270
+ if (
271
+ this.#googleHomeFoyer !== undefined &&
272
+ this.connected === true &&
273
+ this.#id !== undefined &&
274
+ this.#googleHomeDeviceUUID !== undefined
275
+ ) {
276
+ let homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'JoinStream', {
277
+ command: 'extend',
278
+ deviceId: this.uuid,
279
+ streamId: this.#id,
280
+ });
281
+
282
+ if (homeFoyerResponse?.data?.[0]?.streamExtensionStatus !== 'STATUS_STREAM_EXTENDED') {
283
+ this?.log?.debug && this.log.debug('Error occurred while requested stream extension for uuid "%s"', this.uuid);
284
+
285
+ if (typeof this.#peerConnection?.close === 'function') {
286
+ await this.#peerConnection.close();
287
+ }
285
288
  }
286
289
  }
287
- }
288
- }, EXTENDINTERVAL);
290
+ }, EXTENDINTERVAL);
291
+ }
289
292
  }
290
293
  }
291
294
  }
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.2.3",
5
+ "version": "0.2.9",
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",
@@ -45,26 +45,26 @@
45
45
  ],
46
46
  "scripts": {
47
47
  "clean": "rimraf ./dist*",
48
- "format": "prettier --write src/*.js src/**/*.js",
49
- "lint": "eslint src/*.js src/**/*.js --max-warnings=20",
48
+ "format": "prettier --write src/*.js src/**/*.js src/**/*.mjs",
49
+ "lint": "eslint src/*.js src/**/*.js src/**/*.mjs --max-warnings=20",
50
50
  "build": "npm run clean && copyfiles -u 1 src/*.js dist && copyfiles -u 2 src/HomeKitDevice/*.js dist && copyfiles -u 2 src/HomeKitHistory/*.js dist && copyfiles -u 1 src/res/*.h264 dist && copyfiles -u 1 src/res/*.jpg dist && copyfiles -u 1 'src/protobuf/**/*.proto' dist",
51
51
  "prepublishOnly": "npm run lint && npm run build"
52
52
  },
53
53
  "devDependencies": {
54
- "@eslint/js": "^9.16.0",
55
- "@stylistic/eslint-plugin": "^2.11.0",
56
- "@types/node": "^22.10.1",
57
- "@typescript-eslint/parser": "^8.17.0",
58
- "homebridge": "^2.0.0-beta.0",
54
+ "@eslint/js": "^9.23.0",
55
+ "@stylistic/eslint-plugin": "^4.2.0",
56
+ "@types/node": "^22.13.11",
57
+ "@typescript-eslint/parser": "^8.27.0",
59
58
  "copyfiles": "^2.4.1",
60
- "eslint": "^9.16.0",
61
- "prettier": "^3.4.2",
59
+ "eslint": "^9.23.0",
60
+ "homebridge": "^2.0.0-beta.0",
61
+ "prettier": "^3.5.3",
62
62
  "prettier-eslint": "^16.3.0",
63
63
  "rimraf": "^6.0.1"
64
64
  },
65
65
  "dependencies": {
66
66
  "protobufjs": "^7.4.0",
67
- "ws": "^8.18.0",
68
- "werift": "^0.20.1"
67
+ "werift": "^0.22.1",
68
+ "ws": "^8.18.1"
69
69
  }
70
70
  }