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 +10 -0
- package/README.md +14 -13
- package/dist/config.js +5 -3
- package/dist/consts.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nexustalk.js +11 -11
- package/dist/plugins/camera.js +30 -37
- package/dist/plugins/doorbell.js +4 -4
- package/dist/plugins/floodlight.js +3 -3
- package/dist/plugins/lock.js +2 -2
- package/dist/plugins/protect.js +2 -2
- package/dist/plugins/tempsensor.js +2 -2
- package/dist/plugins/thermostat.js +18 -18
- package/dist/plugins/weather.js +3 -3
- package/dist/res/Nest_camera_off.h264 +0 -0
- package/dist/res/Nest_camera_off.jpg +0 -0
- package/dist/res/Nest_camera_offline.h264 +0 -0
- package/dist/res/Nest_camera_offline.jpg +0 -0
- package/dist/res/Nest_camera_transfer.h264 +0 -0
- package/dist/res/Nest_camera_transfer.jpg +0 -0
- package/dist/system.js +9 -9
- package/dist/utils.js +1 -2
- package/dist/webrtc.js +9 -9
- package/package.json +7 -7
- package/dist/rtpmuxer.js +0 -186
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 "
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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"
|
|
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
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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?.(
|
|
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
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,
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
178
|
+
// Close an authenticated socket stream gracefully
|
|
179
179
|
if (this.#socket !== undefined) {
|
|
180
180
|
if (stopStreamFirst === true) {
|
|
181
|
-
// Send a
|
|
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
|
|
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
|
|
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
|
-
//
|
|
433
|
-
// If
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/plugins/camera.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
395
|
+
this.hap.Characteristic.ManuallyDisabled,
|
|
401
396
|
deviceData.streaming_enabled === true
|
|
402
|
-
? this.hap.Characteristic.
|
|
403
|
-
: this.hap.Characteristic.
|
|
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
|
-
//
|
|
429
|
+
// Specific settings for non-HKSV camera's
|
|
435
430
|
this.controller.recordingManagement.operatingModeService.updateCharacteristic(
|
|
436
|
-
this.hap.Characteristic.
|
|
437
|
-
|
|
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.
|
|
444
|
-
|
|
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
|
|
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
|
|
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
|
|
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(' ')
|
|
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
|
|
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
|
|
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
|
|
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(' ')
|
|
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
|
|
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 =
|
|
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 });
|
package/dist/plugins/doorbell.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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: () => {
|
package/dist/plugins/lock.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
package/dist/plugins/protect.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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('
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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 ===
|
|
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(
|
|
760
|
-
let isActive =
|
|
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,
|
|
767
|
+
this.dehumidifierService.updateCharacteristic(this.hap.Characteristic.Active, dehumidifierState);
|
|
768
768
|
|
|
769
769
|
this?.log?.info?.(
|
|
770
|
-
'Set
|
|
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.
|
|
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?.
|
|
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';
|
package/dist/plugins/weather.js
CHANGED
|
@@ -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
|
|
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
|
|
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 '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 '
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 '
|
|
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.
|
|
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.
|
|
58
|
-
"eslint": "^9.
|
|
59
|
-
"@stylistic/eslint-plugin": "^5.
|
|
60
|
-
"@types/node": "^24.
|
|
61
|
-
"@typescript-eslint/parser": "^8.
|
|
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.
|
|
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
|
-
}
|