homebridge-nest-accfactory 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/HomeKitDevice.js +43 -34
- package/dist/config.js +10 -10
- package/dist/devices.js +14 -9
- package/dist/nexustalk.js +28 -28
- package/dist/plugins/camera.js +22 -20
- package/dist/plugins/protect.js +3 -3
- package/dist/plugins/tempsensor.js +3 -3
- package/dist/plugins/thermostat.js +3 -3
- package/dist/streamer.js +27 -27
- package/dist/system.js +79 -93
- package/dist/webrtc.js +12 -12
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
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.1 (2025/06/16)
|
|
6
|
+
|
|
7
|
+
- Minor stability improvements affecting standalone docker version
|
|
8
|
+
|
|
5
9
|
## v0.3.0 (2025/06/14)
|
|
6
10
|
|
|
7
11
|
- General code cleanup and stability improvements
|
package/dist/HomeKitDevice.js
CHANGED
|
@@ -45,30 +45,31 @@ import crypto from 'crypto';
|
|
|
45
45
|
import EventEmitter from 'node:events';
|
|
46
46
|
|
|
47
47
|
// Define constants
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
warn: 'warn',
|
|
55
|
-
error: 'error',
|
|
56
|
-
debug: 'debug',
|
|
48
|
+
const LOG_LEVELS = {
|
|
49
|
+
INFO: 'info',
|
|
50
|
+
SUCCESS: 'success',
|
|
51
|
+
WARN: 'warn',
|
|
52
|
+
ERROR: 'error',
|
|
53
|
+
DEBUG: 'debug',
|
|
57
54
|
};
|
|
58
55
|
|
|
59
56
|
// Define our HomeKit device class
|
|
60
|
-
class HomeKitDevice {
|
|
57
|
+
export default class HomeKitDevice {
|
|
61
58
|
static UPDATE = 'HomeKitDevice.update'; // Device update message
|
|
62
59
|
static REMOVE = 'HomeKitDevice.remove'; // Device remove message
|
|
63
60
|
static SET = 'HomeKitDevice.set'; // Device set property message
|
|
64
61
|
static GET = 'HomeKitDevice.get'; // Device get property message
|
|
65
62
|
|
|
63
|
+
static HK_PIN_3_2_3 = /^\d{3}-\d{2}-\d{3}$/;
|
|
64
|
+
static HK_PIN_4_4 = /^\d{4}-\d{4}$/;
|
|
65
|
+
static MAC_ADDR = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
|
|
66
|
+
|
|
66
67
|
// Override this in the class which extends
|
|
67
68
|
static PLUGIN_NAME = undefined; // Homebridge plugin name
|
|
68
69
|
static PLATFORM_NAME = undefined; // Homebridge platform name
|
|
69
70
|
static HISTORY = undefined; // HomeKit History object
|
|
70
71
|
static TYPE = 'base'; // String naming type of device
|
|
71
|
-
static VERSION = '2025.06.
|
|
72
|
+
static VERSION = '2025.06.15'; // Code version
|
|
72
73
|
|
|
73
74
|
deviceData = {}; // The devices data we store
|
|
74
75
|
historyService = undefined; // HomeKit history service
|
|
@@ -82,9 +83,9 @@ class HomeKitDevice {
|
|
|
82
83
|
#eventEmitter = undefined; // Event emitter to use for comms
|
|
83
84
|
#postSetupDetails = []; // Use for extra output details once a device has been setup
|
|
84
85
|
|
|
85
|
-
constructor(accessory, api, log, eventEmitter, deviceData) {
|
|
86
|
+
constructor(accessory = undefined, api = undefined, log = undefined, eventEmitter = undefined, deviceData = {}) {
|
|
86
87
|
// Validate the passed in logging object. We are expecting certain functions to be present
|
|
87
|
-
if (Object.
|
|
88
|
+
if (Object.values(LOG_LEVELS).every((fn) => typeof log?.[fn] === 'function')) {
|
|
88
89
|
this.log = log;
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -94,14 +95,14 @@ class HomeKitDevice {
|
|
|
94
95
|
this.hap = api.hap;
|
|
95
96
|
this.#platform = api;
|
|
96
97
|
|
|
97
|
-
this.postSetupDetail('Homebridge backend',
|
|
98
|
+
this.postSetupDetail('Homebridge backend', LOG_LEVELS.DEBUG);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
if (typeof api?.HAPLibraryVersion === 'function' && api?.version === undefined && api?.hap === undefined) {
|
|
101
102
|
// As we're missing the Homebridge entry points but have the HAP library version
|
|
102
103
|
this.hap = api;
|
|
103
104
|
|
|
104
|
-
this.postSetupDetail('HAP-NodeJS library',
|
|
105
|
+
this.postSetupDetail('HAP-NodeJS library', LOG_LEVELS.DEBUG);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
// Generate UUID for this device instance
|
|
@@ -157,10 +158,10 @@ class HomeKitDevice {
|
|
|
157
158
|
this.deviceData.manufacturer === '' ||
|
|
158
159
|
(this.#platform === undefined &&
|
|
159
160
|
(typeof this.deviceData?.hkPairingCode !== 'string' ||
|
|
160
|
-
(
|
|
161
|
-
|
|
161
|
+
(HomeKitDevice.HK_PIN_3_2_3.test(this.deviceData.hkPairingCode) === false &&
|
|
162
|
+
HomeKitDevice.HK_PIN_4_4.test(this.deviceData.hkPairingCode) === false) ||
|
|
162
163
|
typeof this.deviceData?.hkUsername !== 'string' ||
|
|
163
|
-
|
|
164
|
+
HomeKitDevice.MAC_ADDR.test(this.deviceData.hkUsername) === false))
|
|
164
165
|
) {
|
|
165
166
|
return;
|
|
166
167
|
}
|
|
@@ -198,7 +199,7 @@ class HomeKitDevice {
|
|
|
198
199
|
|
|
199
200
|
if (typeof this?.setupDevice === 'function') {
|
|
200
201
|
try {
|
|
201
|
-
this.postSetupDetail('Serial number "%s"', this.deviceData.serialNumber,
|
|
202
|
+
this.postSetupDetail('Serial number "%s"', this.deviceData.serialNumber, LOG_LEVELS.DEBUG);
|
|
202
203
|
|
|
203
204
|
await this.setupDevice();
|
|
204
205
|
|
|
@@ -207,13 +208,16 @@ class HomeKitDevice {
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
this?.log?.info?.('Setup %s %s as "%s"', this.deviceData.manufacturer, this.deviceData.model, this.deviceData.description);
|
|
210
|
-
|
|
211
211
|
this.#postSetupDetails.forEach((entry) => {
|
|
212
212
|
if (typeof entry === 'string') {
|
|
213
|
-
this?.log?.[
|
|
213
|
+
this?.log?.[LOG_LEVELS.INFO]?.(' += %s', entry);
|
|
214
214
|
} else if (typeof entry?.message === 'string') {
|
|
215
215
|
let level =
|
|
216
|
-
Object.hasOwn(
|
|
216
|
+
Object.hasOwn(LOG_LEVELS, entry?.level?.toUpperCase?.()) &&
|
|
217
|
+
typeof this?.log?.[LOG_LEVELS[entry.level.toUpperCase()]] === 'function'
|
|
218
|
+
? LOG_LEVELS[entry.level.toUpperCase()]
|
|
219
|
+
: LOG_LEVELS.INFO;
|
|
220
|
+
|
|
217
221
|
this?.log?.[level]?.(' += ' + entry.message, ...(Array.isArray(entry?.args) ? entry.args : []));
|
|
218
222
|
}
|
|
219
223
|
});
|
|
@@ -236,7 +240,7 @@ class HomeKitDevice {
|
|
|
236
240
|
this?.log?.info(' += Advertising as "%s"', this.accessory.displayName);
|
|
237
241
|
this?.log?.info(' += Pairing code is "%s"', this.accessory.pincode);
|
|
238
242
|
}
|
|
239
|
-
this.#postSetupDetails = []; //
|
|
243
|
+
this.#postSetupDetails = []; // Don't need these anymore
|
|
240
244
|
return this.accessory; // Return our HomeKit accessory
|
|
241
245
|
}
|
|
242
246
|
|
|
@@ -499,19 +503,16 @@ class HomeKitDevice {
|
|
|
499
503
|
return;
|
|
500
504
|
}
|
|
501
505
|
|
|
502
|
-
let
|
|
503
|
-
let availableLevel = Object.keys(LOGLEVELS).find((lvl) => typeof this.log?.[lvl] === 'function') || 'info';
|
|
506
|
+
let levelKey = 'INFO';
|
|
504
507
|
let lastArg = args.at(-1);
|
|
505
508
|
|
|
506
|
-
if (typeof lastArg === 'string' && Object.hasOwn(
|
|
507
|
-
|
|
509
|
+
if (typeof lastArg === 'string' && Object.hasOwn(LOG_LEVELS, lastArg.toUpperCase())) {
|
|
510
|
+
levelKey = lastArg.toUpperCase();
|
|
508
511
|
args = args.slice(0, -1);
|
|
509
|
-
} else {
|
|
510
|
-
level = availableLevel;
|
|
511
512
|
}
|
|
512
513
|
|
|
513
514
|
this.#postSetupDetails.push({
|
|
514
|
-
level,
|
|
515
|
+
level: LOG_LEVELS[levelKey], // 'info', 'debug', etc.
|
|
515
516
|
message,
|
|
516
517
|
args: args.length > 0 ? args : undefined,
|
|
517
518
|
});
|
|
@@ -544,8 +545,16 @@ class HomeKitDevice {
|
|
|
544
545
|
|
|
545
546
|
return uuid;
|
|
546
547
|
}
|
|
547
|
-
}
|
|
548
548
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
static makeHomeKitName(name) {
|
|
550
|
+
// Strip invalid characters to meet HomeKit naming requirements
|
|
551
|
+
// Ensure only letters or numbers are at the beginning AND/OR end of string
|
|
552
|
+
// Matches against uni-code characters
|
|
553
|
+
return typeof name === 'string'
|
|
554
|
+
? name
|
|
555
|
+
.replace(/[^\p{L}\p{N}\p{Z}\u2019.,-]/gu, '')
|
|
556
|
+
.replace(/^[^\p{L}\p{N}]*/gu, '')
|
|
557
|
+
.replace(/[^\p{L}\p{N}]+$/gu, '')
|
|
558
|
+
: name;
|
|
559
|
+
}
|
|
560
|
+
}
|
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.06.
|
|
4
|
+
// Code version 2025.06.15
|
|
5
5
|
// Mark Hulskamp
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
@@ -13,10 +13,10 @@ import process from 'node:process';
|
|
|
13
13
|
import child_process from 'node:child_process';
|
|
14
14
|
|
|
15
15
|
// Define constants
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const FFMPEG_VERSION = '6.0.0';
|
|
17
|
+
const ACCOUNT_TYPE = {
|
|
18
|
+
NEST: 'Nest',
|
|
19
|
+
GOOGLE: 'Google',
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
function processConfig(config, log) {
|
|
@@ -80,7 +80,7 @@ function processConfig(config, log) {
|
|
|
80
80
|
options.ffmpeg.libfdk_aac = stdout.includes('--enable-libfdk-aac') === true;
|
|
81
81
|
|
|
82
82
|
let versionTooOld =
|
|
83
|
-
options.ffmpeg.version?.localeCompare(
|
|
83
|
+
options.ffmpeg.version?.localeCompare(FFMPEG_VERSION, undefined, {
|
|
84
84
|
numeric: true,
|
|
85
85
|
sensitivity: 'case',
|
|
86
86
|
caseFirst: 'upper',
|
|
@@ -96,7 +96,7 @@ function processConfig(config, log) {
|
|
|
96
96
|
log?.warn?.('ffmpeg binary "%s" does not meet the minimum support requirements', options.ffmpeg.binary);
|
|
97
97
|
|
|
98
98
|
if (versionTooOld) {
|
|
99
|
-
log?.warn?.('Minimum binary version is "%s", however the installed version is "%s"',
|
|
99
|
+
log?.warn?.('Minimum binary version is "%s", however the installed version is "%s"', FFMPEG_VERSION, options.ffmpeg.version);
|
|
100
100
|
log?.warn?.('Stream video/recording from camera/doorbells will be unavailable');
|
|
101
101
|
options.ffmpeg.binary = undefined; // No ffmpeg since below min version
|
|
102
102
|
}
|
|
@@ -167,7 +167,7 @@ function buildConnections(config) {
|
|
|
167
167
|
let fieldTest = section?.fieldTest === true;
|
|
168
168
|
connections[crypto.randomUUID()] = {
|
|
169
169
|
name: key,
|
|
170
|
-
type:
|
|
170
|
+
type: ACCOUNT_TYPE.NEST,
|
|
171
171
|
authorised: false,
|
|
172
172
|
access_token: section.access_token,
|
|
173
173
|
fieldTest,
|
|
@@ -187,7 +187,7 @@ function buildConnections(config) {
|
|
|
187
187
|
let fieldTest = section?.fieldTest === true;
|
|
188
188
|
connections[crypto.randomUUID()] = {
|
|
189
189
|
name: key,
|
|
190
|
-
type:
|
|
190
|
+
type: ACCOUNT_TYPE.GOOGLE,
|
|
191
191
|
authorised: false,
|
|
192
192
|
issuetoken: section.issuetoken,
|
|
193
193
|
cookie: section.cookie,
|
|
@@ -204,4 +204,4 @@ function buildConnections(config) {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
// Define exports
|
|
207
|
-
export {
|
|
207
|
+
export { ACCOUNT_TYPE, processConfig, buildConnections };
|
package/dist/devices.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Device support loader
|
|
2
2
|
// Part of homebridge-nest-accfactory
|
|
3
3
|
//
|
|
4
|
-
// Code version 2025.06.
|
|
4
|
+
// Code version 2025.06.15
|
|
5
5
|
// Mark Hulskamp
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ import HomeKitDevice from './HomeKitDevice.js';
|
|
|
15
15
|
|
|
16
16
|
// Define constants
|
|
17
17
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
18
|
-
const
|
|
18
|
+
const DEVICE_TYPE = Object.freeze({
|
|
19
19
|
THERMOSTAT: 'Thermostat',
|
|
20
20
|
TEMPSENSOR: 'TemperatureSensor',
|
|
21
21
|
SMOKESENSOR: 'Protect',
|
|
@@ -82,27 +82,32 @@ async function loadDeviceModules(log, pluginDir = '') {
|
|
|
82
82
|
function getDeviceHKCategory(type) {
|
|
83
83
|
let category = 1; // Categories.OTHER
|
|
84
84
|
|
|
85
|
-
if (type ===
|
|
85
|
+
if (type === DEVICE_TYPE.LOCK) {
|
|
86
86
|
category = 6; // Categories.DOOR_LOCK
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
if (type ===
|
|
89
|
+
if (type === DEVICE_TYPE.THERMOSTAT) {
|
|
90
90
|
category = 9; // Categories.THERMOSTAT
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
if (
|
|
93
|
+
if (
|
|
94
|
+
type === DEVICE_TYPE.TEMPSENSOR ||
|
|
95
|
+
type === DEVICE_TYPE.HEATLINK ||
|
|
96
|
+
type === DEVICE_TYPE.SMOKESENSOR ||
|
|
97
|
+
type === DEVICE_TYPE.WEATHER
|
|
98
|
+
) {
|
|
94
99
|
category = 10; // Categories.SENSOR
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
if (type ===
|
|
102
|
+
if (type === DEVICE_TYPE.ALARM) {
|
|
98
103
|
category = 11; // Categories.SECURITY_SYSTEM
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
if (type ===
|
|
106
|
+
if (type === DEVICE_TYPE.CAMERA || type === DEVICE_TYPE.FLOODLIGHT) {
|
|
102
107
|
category = 17; // Categories.IP_CAMERA
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
if (type ===
|
|
110
|
+
if (type === DEVICE_TYPE.DOORBELL) {
|
|
106
111
|
category = 18; // Categories.VIDEO_DOORBELL
|
|
107
112
|
}
|
|
108
113
|
|
|
@@ -110,4 +115,4 @@ function getDeviceHKCategory(type) {
|
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
// Define exports
|
|
113
|
-
export {
|
|
118
|
+
export { DEVICE_TYPE, loadDeviceModules, getDeviceHKCategory };
|
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.06.
|
|
8
|
+
// Code version 2025.06.15
|
|
9
9
|
// Mark Hulskamp
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
@@ -25,11 +25,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
25
25
|
import Streamer from './streamer.js';
|
|
26
26
|
|
|
27
27
|
// Define constants
|
|
28
|
-
const
|
|
29
|
-
const
|
|
28
|
+
const PING_INTERVAL = 15000; // Ping interval to nexus server while stream active
|
|
29
|
+
const USER_AGENT = 'Nest/5.78.0 (iOScom.nestlabs.jasper.release) os=18.0'; // User Agent string
|
|
30
30
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const PACKET_TYPE = {
|
|
33
33
|
PING: 1,
|
|
34
34
|
HELLO: 100,
|
|
35
35
|
PING_CAMERA: 101,
|
|
@@ -56,7 +56,7 @@ const PacketType = {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
// Blank audio in AAC format, mono channel @48000
|
|
59
|
-
const
|
|
59
|
+
const AAC_MONO_48000_BLANK = Buffer.from([
|
|
60
60
|
0xff, 0xf1, 0x4c, 0x40, 0x03, 0x9f, 0xfc, 0xde, 0x02, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x39, 0x2e, 0x31, 0x38, 0x2e, 0x31, 0x30, 0x30,
|
|
61
61
|
0x00, 0x02, 0x30, 0x40, 0x0e,
|
|
62
62
|
]);
|
|
@@ -68,7 +68,7 @@ export default class NexusTalk extends Streamer {
|
|
|
68
68
|
pingTimer = undefined; // Timer object for ping interval
|
|
69
69
|
stalledTimer = undefined; // Timer object for no received data
|
|
70
70
|
host = ''; // Host to connect to or connected too
|
|
71
|
-
blankAudio =
|
|
71
|
+
blankAudio = AAC_MONO_48000_BLANK;
|
|
72
72
|
video = {}; // Video stream details once connected
|
|
73
73
|
audio = {}; // Audio stream details once connected
|
|
74
74
|
|
|
@@ -219,7 +219,7 @@ export default class NexusTalk extends Streamer {
|
|
|
219
219
|
sampleRate: 16000,
|
|
220
220
|
}),
|
|
221
221
|
).finish();
|
|
222
|
-
this.#sendMessage(
|
|
222
|
+
this.#sendMessage(PACKET_TYPE.AUDIO_PAYLOAD, encodedData);
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
}
|
|
@@ -248,7 +248,7 @@ export default class NexusTalk extends Streamer {
|
|
|
248
248
|
profileNotFoundAction: 'REDIRECT',
|
|
249
249
|
}),
|
|
250
250
|
).finish();
|
|
251
|
-
this.#sendMessage(
|
|
251
|
+
this.#sendMessage(PACKET_TYPE.START_PLAYBACK, encodedData);
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -261,13 +261,13 @@ export default class NexusTalk extends Streamer {
|
|
|
261
261
|
sessionId: this.#id,
|
|
262
262
|
}),
|
|
263
263
|
).finish();
|
|
264
|
-
this.#sendMessage(
|
|
264
|
+
this.#sendMessage(PACKET_TYPE.STOP_PLAYBACK, encodedData);
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
#sendMessage(type, data) {
|
|
270
|
-
if (this.#socket?.readyState !== 'open' || (type !==
|
|
270
|
+
if (this.#socket?.readyState !== 'open' || (type !== PACKET_TYPE.HELLO && this.#authorised === false)) {
|
|
271
271
|
// We're not connect and/or authorised yet, so 'cache' message for processing once this occurs
|
|
272
272
|
this.#messages.push({ type: type, data: data });
|
|
273
273
|
return;
|
|
@@ -275,11 +275,11 @@ export default class NexusTalk extends Streamer {
|
|
|
275
275
|
|
|
276
276
|
// Create nexusTalk message header
|
|
277
277
|
let header = Buffer.alloc(3);
|
|
278
|
-
if (type !==
|
|
278
|
+
if (type !== PACKET_TYPE.LONG_PLAYBACK_PACKET) {
|
|
279
279
|
header.writeUInt8(type, 0);
|
|
280
280
|
header.writeUInt16BE(data.length, 1);
|
|
281
281
|
}
|
|
282
|
-
if (type ===
|
|
282
|
+
if (type === PACKET_TYPE.LONG_PLAYBACK_PACKET) {
|
|
283
283
|
header = Buffer.alloc(5);
|
|
284
284
|
header.writeUInt8(type, 0);
|
|
285
285
|
header.writeUInt32BE(data.length, 1);
|
|
@@ -309,7 +309,7 @@ export default class NexusTalk extends Streamer {
|
|
|
309
309
|
if (reauthorise === true && authoriseRequest !== null) {
|
|
310
310
|
// Request to re-authorise only
|
|
311
311
|
this?.log?.debug?.('Re-authentication requested to "%s"', this.host);
|
|
312
|
-
this.#sendMessage(
|
|
312
|
+
this.#sendMessage(PACKET_TYPE.AUTHORIZE_REQUEST, authoriseRequest);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
if (reauthorise === false && authoriseRequest !== null) {
|
|
@@ -323,13 +323,13 @@ export default class NexusTalk extends Streamer {
|
|
|
323
323
|
protocolVersion: 'VERSION_3',
|
|
324
324
|
uuid: this.uuid.split(/[._]+/)[1],
|
|
325
325
|
requireConnectedCamera: false,
|
|
326
|
-
|
|
326
|
+
USER_AGENT: USER_AGENT,
|
|
327
327
|
deviceId: crypto.randomUUID(),
|
|
328
328
|
clientType: 'IOS',
|
|
329
329
|
authoriseRequest: authoriseRequest,
|
|
330
330
|
}),
|
|
331
331
|
).finish();
|
|
332
|
-
this.#sendMessage(
|
|
332
|
+
this.#sendMessage(PACKET_TYPE.HELLO, encodedData);
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
}
|
|
@@ -494,7 +494,7 @@ export default class NexusTalk extends Streamer {
|
|
|
494
494
|
let packetType = this.#packets.readUInt8(0);
|
|
495
495
|
let packetSize = this.#packets.readUInt16BE(1);
|
|
496
496
|
|
|
497
|
-
if (packetType ===
|
|
497
|
+
if (packetType === PACKET_TYPE.LONG_PLAYBACK_PACKET) {
|
|
498
498
|
headerSize = 5;
|
|
499
499
|
packetSize = this.#packets.readUInt32BE(1);
|
|
500
500
|
}
|
|
@@ -509,11 +509,11 @@ export default class NexusTalk extends Streamer {
|
|
|
509
509
|
this.#packets = this.#packets.subarray(headerSize + packetSize);
|
|
510
510
|
|
|
511
511
|
switch (packetType) {
|
|
512
|
-
case
|
|
512
|
+
case PACKET_TYPE.PING: {
|
|
513
513
|
break;
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
case
|
|
516
|
+
case PACKET_TYPE.OK: {
|
|
517
517
|
// process any pending messages we have stored
|
|
518
518
|
this.#authorised = true; // OK message, means we're connected and authorised to Nexus
|
|
519
519
|
for (let message = this.#messages.shift(); message; message = this.#messages.shift()) {
|
|
@@ -523,46 +523,46 @@ export default class NexusTalk extends Streamer {
|
|
|
523
523
|
// Periodically send PING message to keep stream alive
|
|
524
524
|
clearInterval(this.pingTimer);
|
|
525
525
|
this.pingTimer = setInterval(() => {
|
|
526
|
-
this.#sendMessage(
|
|
527
|
-
},
|
|
526
|
+
this.#sendMessage(PACKET_TYPE.PING, Buffer.alloc(0));
|
|
527
|
+
}, PING_INTERVAL);
|
|
528
528
|
|
|
529
529
|
// Start processing data
|
|
530
530
|
this.#startNexusData();
|
|
531
531
|
break;
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
-
case
|
|
534
|
+
case PACKET_TYPE.ERROR: {
|
|
535
535
|
this.#handleNexusError(protoBufPayload);
|
|
536
536
|
break;
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
-
case
|
|
539
|
+
case PACKET_TYPE.PLAYBACK_BEGIN: {
|
|
540
540
|
this.#handlePlaybackBegin(protoBufPayload);
|
|
541
541
|
break;
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
-
case
|
|
544
|
+
case PACKET_TYPE.PLAYBACK_END: {
|
|
545
545
|
this.#handlePlaybackEnd(protoBufPayload);
|
|
546
546
|
break;
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
case
|
|
550
|
-
case
|
|
549
|
+
case PACKET_TYPE.PLAYBACK_PACKET:
|
|
550
|
+
case PACKET_TYPE.LONG_PLAYBACK_PACKET: {
|
|
551
551
|
this.#handlePlaybackPacket(protoBufPayload);
|
|
552
552
|
break;
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
-
case
|
|
555
|
+
case PACKET_TYPE.REDIRECT: {
|
|
556
556
|
this.#handleRedirect(protoBufPayload);
|
|
557
557
|
break;
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
-
case
|
|
560
|
+
case PACKET_TYPE.TALKBACK_BEGIN: {
|
|
561
561
|
this.#handleTalkbackBegin(protoBufPayload);
|
|
562
562
|
break;
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
-
case
|
|
565
|
+
case PACKET_TYPE.TALKBACK_END: {
|
|
566
566
|
this.#handleTalkbackEnd(protoBufPayload);
|
|
567
567
|
break;
|
|
568
568
|
}
|
package/dist/plugins/camera.js
CHANGED
|
@@ -21,21 +21,23 @@ import HomeKitDevice from '../HomeKitDevice.js';
|
|
|
21
21
|
import NexusTalk from '../nexustalk.js';
|
|
22
22
|
import WebRTC from '../webrtc.js';
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const CAMERA_RESOURCE = {
|
|
25
|
+
OFFLINE: 'Nest_camera_offline.jpg',
|
|
26
|
+
OFF: 'Nest_camera_off.jpg',
|
|
27
|
+
TRANSFER: 'Nest_camera_transfer.jpg',
|
|
28
28
|
};
|
|
29
29
|
const MP4BOX = 'mp4box'; // MP4 box fragement event for HKSV recording
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const SNAPSHOT_CACHE_TIMEOUT = 30000; // Timeout for retaining snapshot image (in milliseconds)
|
|
31
|
+
const STREAMING_PROTOCOL = {
|
|
32
|
+
WEBRTC: 'PROTOCOL_WEBRTC',
|
|
33
|
+
NEXUSTALK: 'PROTOCOL_NEXUSTALK',
|
|
34
|
+
};
|
|
35
|
+
const RESOURCE_PATH = '../res';
|
|
34
36
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
35
37
|
|
|
36
38
|
export default class NestCamera extends HomeKitDevice {
|
|
37
39
|
static TYPE = 'Camera';
|
|
38
|
-
static VERSION = '2025.06.
|
|
40
|
+
static VERSION = '2025.06.15';
|
|
39
41
|
|
|
40
42
|
controller = undefined; // HomeKit Camera/Doorbell controller service
|
|
41
43
|
streamer = undefined; // Streamer object for live/recording stream
|
|
@@ -61,7 +63,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
61
63
|
// Load supporrt image files as required
|
|
62
64
|
const loadImageIfExists = (filename, label) => {
|
|
63
65
|
let buffer = undefined;
|
|
64
|
-
let file = path.resolve(__dirname,
|
|
66
|
+
let file = path.resolve(__dirname, RESOURCE_PATH, filename);
|
|
65
67
|
if (fs.existsSync(file) === true) {
|
|
66
68
|
buffer = fs.readFileSync(file);
|
|
67
69
|
} else {
|
|
@@ -70,9 +72,9 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
70
72
|
return buffer;
|
|
71
73
|
};
|
|
72
74
|
|
|
73
|
-
this.#cameraOfflineImage = loadImageIfExists(
|
|
74
|
-
this.#cameraVideoOffImage = loadImageIfExists(
|
|
75
|
-
this.#cameraTransferringImage = loadImageIfExists(
|
|
75
|
+
this.#cameraOfflineImage = loadImageIfExists(CAMERA_RESOURCE.OFFLINE, 'offline');
|
|
76
|
+
this.#cameraVideoOffImage = loadImageIfExists(CAMERA_RESOURCE.OFF, 'video off');
|
|
77
|
+
this.#cameraTransferringImage = loadImageIfExists(CAMERA_RESOURCE.TRANSFER, 'transferring');
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
// Class functions
|
|
@@ -201,10 +203,10 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
if (
|
|
204
|
-
(this.deviceData.streaming_protocols.includes(
|
|
205
|
-
this.deviceData.streaming_protocols.includes(
|
|
206
|
-
(this.deviceData.streaming_protocols.includes(
|
|
207
|
-
(this.deviceData.streaming_protocols.includes(
|
|
206
|
+
(this.deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.WEBRTC) === false &&
|
|
207
|
+
this.deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.NEXUSTALK) === false) ||
|
|
208
|
+
(this.deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.WEBRTC) === true && WebRTC === undefined) ||
|
|
209
|
+
(this.deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.NEXUSTALK) === true && NexusTalk === undefined)
|
|
208
210
|
) {
|
|
209
211
|
this?.log?.error?.(
|
|
210
212
|
'No suitable streaming protocol is present for "%s". Streaming and recording will be unavailable',
|
|
@@ -556,7 +558,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
556
558
|
clearTimeout(this.snapshotTimer);
|
|
557
559
|
this.snapshotTimer = setTimeout(() => {
|
|
558
560
|
this.lastSnapshotImage = undefined;
|
|
559
|
-
},
|
|
561
|
+
}, SNAPSHOT_CACHE_TIMEOUT);
|
|
560
562
|
}
|
|
561
563
|
}
|
|
562
564
|
|
|
@@ -1009,7 +1011,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1009
1011
|
this.streamer !== undefined && this.streamer.stopEverything();
|
|
1010
1012
|
this.streamer = undefined;
|
|
1011
1013
|
}
|
|
1012
|
-
if (deviceData.streaming_protocols.includes(
|
|
1014
|
+
if (deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.WEBRTC) === true && WebRTC !== undefined) {
|
|
1013
1015
|
this?.log?.debug?.('Using WebRTC streamer for "%s"', deviceData.description);
|
|
1014
1016
|
this.streamer = new WebRTC(deviceData, {
|
|
1015
1017
|
log: this.log,
|
|
@@ -1021,7 +1023,7 @@ export default class NestCamera extends HomeKitDevice {
|
|
|
1021
1023
|
});
|
|
1022
1024
|
}
|
|
1023
1025
|
|
|
1024
|
-
if (deviceData.streaming_protocols.includes(
|
|
1026
|
+
if (deviceData.streaming_protocols.includes(STREAMING_PROTOCOL.NEXUSTALK) === true && NexusTalk !== undefined) {
|
|
1025
1027
|
this?.log?.debug?.('Using NexusTalk streamer for "%s"', deviceData.description);
|
|
1026
1028
|
this.streamer = new NexusTalk(deviceData, {
|
|
1027
1029
|
log: this.log,
|
package/dist/plugins/protect.js
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// Define our modules
|
|
8
8
|
import HomeKitDevice from '../HomeKitDevice.js';
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const LOW_BATTERY_LEVEL = 10; // Low battery level percentage
|
|
11
11
|
|
|
12
12
|
export default class NestProtect extends HomeKitDevice {
|
|
13
13
|
static TYPE = 'Protect';
|
|
14
|
-
static VERSION = '2025.06.
|
|
14
|
+
static VERSION = '2025.06.15';
|
|
15
15
|
|
|
16
16
|
batteryService = undefined;
|
|
17
17
|
smokeService = undefined;
|
|
@@ -79,7 +79,7 @@ export default class NestProtect extends HomeKitDevice {
|
|
|
79
79
|
this.batteryService.updateCharacteristic(this.hap.Characteristic.BatteryLevel, deviceData.battery_level);
|
|
80
80
|
this.batteryService.updateCharacteristic(
|
|
81
81
|
this.hap.Characteristic.StatusLowBattery,
|
|
82
|
-
deviceData.battery_level >
|
|
82
|
+
deviceData.battery_level > LOW_BATTERY_LEVEL && deviceData.battery_health_state === 0
|
|
83
83
|
? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
|
|
84
84
|
: this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW,
|
|
85
85
|
);
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// Define our modules
|
|
8
8
|
import HomeKitDevice from '../HomeKitDevice.js';
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const LOW_BATTERY_LEVEL = 10; // Low battery level percentage
|
|
11
11
|
|
|
12
12
|
export default class NestTemperatureSensor extends HomeKitDevice {
|
|
13
13
|
static TYPE = 'TemperatureSensor';
|
|
14
|
-
static VERSION = '2025.06.
|
|
14
|
+
static VERSION = '2025.06.15';
|
|
15
15
|
|
|
16
16
|
batteryService = undefined;
|
|
17
17
|
temperatureService = undefined;
|
|
@@ -70,7 +70,7 @@ export default class NestTemperatureSensor extends HomeKitDevice {
|
|
|
70
70
|
this.batteryService.updateCharacteristic(this.hap.Characteristic.BatteryLevel, deviceData.battery_level);
|
|
71
71
|
this.batteryService.updateCharacteristic(
|
|
72
72
|
this.hap.Characteristic.StatusLowBattery,
|
|
73
|
-
deviceData.battery_level >
|
|
73
|
+
deviceData.battery_level > LOW_BATTERY_LEVEL
|
|
74
74
|
? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
|
|
75
75
|
: this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW,
|
|
76
76
|
);
|
|
@@ -13,14 +13,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
13
|
import HomeKitDevice from '../HomeKitDevice.js';
|
|
14
14
|
|
|
15
15
|
// Define constants
|
|
16
|
-
const
|
|
16
|
+
const LOW_BATTERY_LEVEL = 10; // Low battery level percentage
|
|
17
17
|
const MIN_TEMPERATURE = 9; // Minimum temperature for Nest Thermostat
|
|
18
18
|
const MAX_TEMPERATURE = 32; // Maximum temperature for Nest Thermostat
|
|
19
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
20
20
|
|
|
21
21
|
export default class NestThermostat extends HomeKitDevice {
|
|
22
22
|
static TYPE = 'Thermostat';
|
|
23
|
-
static VERSION = '2025.06.
|
|
23
|
+
static VERSION = '2025.06.15';
|
|
24
24
|
|
|
25
25
|
batteryService = undefined;
|
|
26
26
|
occupancyService = undefined;
|
|
@@ -564,7 +564,7 @@ export default class NestThermostat extends HomeKitDevice {
|
|
|
564
564
|
this.batteryService.updateCharacteristic(this.hap.Characteristic.BatteryLevel, deviceData.battery_level);
|
|
565
565
|
this.batteryService.updateCharacteristic(
|
|
566
566
|
this.hap.Characteristic.StatusLowBattery,
|
|
567
|
-
deviceData.battery_level >
|
|
567
|
+
deviceData.battery_level > LOW_BATTERY_LEVEL
|
|
568
568
|
? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
|
|
569
569
|
: this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW,
|
|
570
570
|
);
|