homebridge-nest-accfactory 0.2.11 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +14 -7
  3. package/config.schema.json +118 -0
  4. package/dist/HomeKitDevice.js +194 -77
  5. package/dist/HomeKitHistory.js +1 -1
  6. package/dist/config.js +207 -0
  7. package/dist/devices.js +113 -0
  8. package/dist/index.js +2 -1
  9. package/dist/nexustalk.js +19 -21
  10. package/dist/{camera.js → plugins/camera.js} +212 -239
  11. package/dist/{doorbell.js → plugins/doorbell.js} +32 -30
  12. package/dist/plugins/floodlight.js +91 -0
  13. package/dist/plugins/heatlink.js +17 -0
  14. package/dist/{protect.js → plugins/protect.js} +24 -41
  15. package/dist/{tempsensor.js → plugins/tempsensor.js} +13 -17
  16. package/dist/{thermostat.js → plugins/thermostat.js} +424 -381
  17. package/dist/{weather.js → plugins/weather.js} +26 -60
  18. package/dist/protobuf/nest/services/apigateway.proto +31 -1
  19. package/dist/protobuf/nest/trait/firmware.proto +207 -89
  20. package/dist/protobuf/nest/trait/hvac.proto +1052 -312
  21. package/dist/protobuf/nest/trait/located.proto +51 -8
  22. package/dist/protobuf/nest/trait/network.proto +366 -36
  23. package/dist/protobuf/nest/trait/occupancy.proto +145 -17
  24. package/dist/protobuf/nest/trait/product/protect.proto +57 -43
  25. package/dist/protobuf/nest/trait/resourcedirectory.proto +8 -0
  26. package/dist/protobuf/nest/trait/sensor.proto +7 -1
  27. package/dist/protobuf/nest/trait/service.proto +3 -1
  28. package/dist/protobuf/nest/trait/structure.proto +60 -14
  29. package/dist/protobuf/nest/trait/ui.proto +41 -1
  30. package/dist/protobuf/nest/trait/user.proto +6 -1
  31. package/dist/protobuf/nest/trait/voiceassistant.proto +2 -1
  32. package/dist/protobuf/nestlabs/eventingapi/v1.proto +20 -1
  33. package/dist/protobuf/root.proto +1 -0
  34. package/dist/protobuf/wdl.proto +18 -2
  35. package/dist/protobuf/weave/common.proto +2 -1
  36. package/dist/protobuf/weave/trait/heartbeat.proto +41 -1
  37. package/dist/protobuf/weave/trait/power.proto +1 -0
  38. package/dist/protobuf/weave/trait/security.proto +10 -1
  39. package/dist/streamer.js +68 -72
  40. package/dist/system.js +1208 -1245
  41. package/dist/webrtc.js +28 -23
  42. package/package.json +12 -12
  43. package/dist/floodlight.js +0 -97
package/dist/config.js ADDED
@@ -0,0 +1,207 @@
1
+ // Configuration validation and processing
2
+ // Part of homebridge-nest-accfactory
3
+ //
4
+ // Code version 2025.06.12
5
+ // Mark Hulskamp
6
+ 'use strict';
7
+
8
+ // Define nodejs module requirements
9
+ import fs from 'fs';
10
+ import path from 'node:path';
11
+ import crypto from 'node:crypto';
12
+ import process from 'node:process';
13
+ import child_process from 'node:child_process';
14
+
15
+ // Define constants
16
+ const FFMPEGVERSION = '6.0.0';
17
+ const AccountType = {
18
+ Nest: 'Nest',
19
+ Google: 'Google',
20
+ };
21
+
22
+ function processConfig(config, log) {
23
+ let options = (config.options = typeof config?.options === 'object' ? config.options : {});
24
+
25
+ options.eveHistory = config.options?.eveHistory === true;
26
+ options.weather = config.options?.weather === true;
27
+ options.hksv = config.options?.hksv === true;
28
+ options.exclude = config.options?.exclude === true;
29
+
30
+ options.elevation =
31
+ isNaN(config.options?.elevation) === false && Number(config.options.elevation) >= 0 && Number(config.options.elevation) <= 8848
32
+ ? Number(config.options.elevation)
33
+ : 0;
34
+
35
+ // Controls what APIs we use, default is to use both Nest and protobuf APIs
36
+ options.useNestAPI = config.options?.useNestAPI === true || config.options?.useNestAPI === undefined;
37
+ options.useGoogleAPI = config.options?.useGoogleAPI === true || config.options?.useGoogleAPI === undefined;
38
+
39
+ // Get configuration for max number of concurrent 'live view' streams. For HomeKit Secure Video, this will always be 1
40
+ options.maxStreams = isNaN(config.options?.maxStreams) === false ? Number(config.options.maxStreams) : 2;
41
+
42
+ // Check if a ffmpeg binary exist via a specific path in configuration OR /usr/local/bin
43
+ options.ffmpeg = {};
44
+ options.ffmpeg.debug = config.options?.ffmpegDebug === true;
45
+ options.ffmpeg.binary = path.resolve(
46
+ typeof config.options?.ffmpegPath === 'string' && config.options.ffmpegPath !== '' ? config.options.ffmpegPath : '/usr/local/bin',
47
+ );
48
+
49
+ // If the path doesn't include 'ffmpeg' on the end, we'll add it here
50
+ if (options.ffmpeg.binary.endsWith('/ffmpeg') === false) {
51
+ options.ffmpeg.binary += '/ffmpeg';
52
+ }
53
+
54
+ options.ffmpeg.version = undefined;
55
+ options.ffmpeg.libspeex = false;
56
+ options.ffmpeg.libopus = false;
57
+ options.ffmpeg.libx264 = false;
58
+ options.ffmpeg.libfdk_aac = false;
59
+
60
+ if (fs.existsSync(options.ffmpeg.binary) === false) {
61
+ // If we flag ffmpegPath as undefined, no video streaming/record support enabled for camers/doorbells
62
+ log?.warn?.('Specified ffmpeg binary "%s" was not found', options.ffmpeg.binary);
63
+ log?.warn?.('Stream video/recording from camera/doorbells will be unavailable');
64
+ options.ffmpeg.binary = undefined;
65
+ }
66
+
67
+ if (fs.existsSync(options.ffmpeg.binary) === true) {
68
+ let ffmpegProcess = child_process.spawnSync(options.ffmpeg.binary, ['-version'], {
69
+ env: process.env,
70
+ });
71
+
72
+ if (ffmpegProcess.stdout !== null) {
73
+ let stdout = ffmpegProcess.stdout.toString();
74
+
75
+ // Determine what libraries ffmpeg is compiled with
76
+ options.ffmpeg.version = stdout.match(/(?:ffmpeg version:(\d+)\.)?(?:(\d+)\.)?(?:(\d+)\.\d+)(.*?)/gim)?.[0];
77
+ options.ffmpeg.libspeex = stdout.includes('--enable-libspeex') === true;
78
+ options.ffmpeg.libopus = stdout.includes('--enable-libopus') === true;
79
+ options.ffmpeg.libx264 = stdout.includes('--enable-libx264') === true;
80
+ options.ffmpeg.libfdk_aac = stdout.includes('--enable-libfdk-aac') === true;
81
+
82
+ let versionTooOld =
83
+ options.ffmpeg.version?.localeCompare(FFMPEGVERSION, undefined, {
84
+ numeric: true,
85
+ sensitivity: 'case',
86
+ caseFirst: 'upper',
87
+ }) === -1;
88
+
89
+ if (
90
+ versionTooOld ||
91
+ options.ffmpeg.libspeex === false ||
92
+ options.ffmpeg.libopus === false ||
93
+ options.ffmpeg.libx264 === false ||
94
+ options.ffmpeg.libfdk_aac === false
95
+ ) {
96
+ log?.warn?.('ffmpeg binary "%s" does not meet the minimum support requirements', options.ffmpeg.binary);
97
+
98
+ if (versionTooOld) {
99
+ log?.warn?.('Minimum binary version is "%s", however the installed version is "%s"', FFMPEGVERSION, options.ffmpeg.version);
100
+ log?.warn?.('Stream video/recording from camera/doorbells will be unavailable');
101
+ options.ffmpeg.binary = undefined; // No ffmpeg since below min version
102
+ }
103
+
104
+ if (!options.ffmpeg.libspeex && options.ffmpeg.libx264 && options.ffmpeg.libfdk_aac) {
105
+ log?.warn?.('Missing libspeex in ffmpeg binary, talkback on certain camera/doorbells will be unavailable');
106
+ }
107
+
108
+ if (options.ffmpeg.libx264 && !options.ffmpeg.libfdk_aac && !options.ffmpeg.libopus) {
109
+ log?.warn?.('Missing libfdk_aac and libopus in ffmpeg binary, audio from camera/doorbells will be unavailable');
110
+ }
111
+
112
+ if (options.ffmpeg.libx264 && !options.ffmpeg.libfdk_aac) {
113
+ log?.warn?.('Missing libfdk_aac in ffmpeg binary, audio from camera/doorbells will be unavailable');
114
+ }
115
+
116
+ if (options.ffmpeg.libx264 && options.ffmpeg.libfdk_aac && !options.ffmpeg.libopus) {
117
+ log?.warn?.('Missing libopus in ffmpeg binary, audio (including talkback) from certain camera/doorbells will be unavailable');
118
+ }
119
+
120
+ if (!options.ffmpeg.libx264) {
121
+ log?.warn?.('Missing libx264 in ffmpeg binary, stream video/recording from camera/doorbells will be unavailable');
122
+ options.ffmpeg.binary = undefined; // No ffmpeg since we do not have all the required libraries
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ if (options.ffmpeg.binary !== undefined) {
129
+ log?.success?.('Found valid ffmpeg binary in %s', options.ffmpeg.binary);
130
+ }
131
+
132
+ // Process per device configuration(s)
133
+ if (config?.devices === undefined) {
134
+ config.devices = [];
135
+ }
136
+
137
+ if (config?.devices !== undefined && Array.isArray(config.devices) === false) {
138
+ // If the devices section is a JSON oject keyed by the devices serial number, convert to devices array object
139
+ let newDeviceArray = [];
140
+ for (const [serialNumber, props] of Object.entries(config.devices)) {
141
+ newDeviceArray.push({
142
+ serialNumber,
143
+ ...props,
144
+ });
145
+ }
146
+ config.devices = newDeviceArray;
147
+
148
+ // Alert user to changed configuration for them to update config
149
+ log?.warn?.('');
150
+ log?.warn?.('NOTICE');
151
+ log?.warn?.('> The per device configuration contains legacy options. Please review the readme at the link below');
152
+ log?.warn?.('> Consider updating your configuration file as the mapping from legacy to current per device configuration maybe removed');
153
+ log?.warn?.('> https://github.com/n0rt0nthec4t/homebridge-nest-accfactory/blob/main/src/README.md');
154
+ log?.warn?.('');
155
+ }
156
+
157
+ return config;
158
+ }
159
+
160
+ function buildConnections(config) {
161
+ let connections = {};
162
+
163
+ Object.keys(config).forEach((key) => {
164
+ let section = config[key];
165
+
166
+ if (typeof section?.access_token === 'string' && section.access_token !== '') {
167
+ let fieldTest = section?.fieldTest === true;
168
+ connections[crypto.randomUUID()] = {
169
+ name: key,
170
+ type: AccountType.Nest,
171
+ authorised: false,
172
+ access_token: section.access_token,
173
+ fieldTest,
174
+ referer: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
175
+ restAPIHost: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
176
+ cameraAPIHost: fieldTest ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
177
+ protobufAPIHost: fieldTest ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
178
+ };
179
+ }
180
+
181
+ if (
182
+ typeof section?.issuetoken === 'string' &&
183
+ section.issuetoken !== '' &&
184
+ typeof section?.cookie === 'string' &&
185
+ section.cookie !== ''
186
+ ) {
187
+ let fieldTest = section?.fieldTest === true;
188
+ connections[crypto.randomUUID()] = {
189
+ name: key,
190
+ type: AccountType.Google,
191
+ authorised: false,
192
+ issuetoken: section.issuetoken,
193
+ cookie: section.cookie,
194
+ fieldTest,
195
+ referer: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
196
+ restAPIHost: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
197
+ cameraAPIHost: fieldTest ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
198
+ protobufAPIHost: fieldTest ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
199
+ };
200
+ }
201
+ });
202
+
203
+ return connections;
204
+ }
205
+
206
+ // Define exports
207
+ export { AccountType, processConfig, buildConnections };
@@ -0,0 +1,113 @@
1
+ // Device support loader
2
+ // Part of homebridge-nest-accfactory
3
+ //
4
+ // Code version 2025.06.12
5
+ // Mark Hulskamp
6
+ 'use strict';
7
+
8
+ // Define nodejs module requirements
9
+ import path from 'node:path';
10
+ import fs from 'node:fs/promises';
11
+ import url from 'node:url';
12
+
13
+ // Import our modules
14
+ import HomeKitDevice from './HomeKitDevice.js';
15
+
16
+ // Define constants
17
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
18
+ const DeviceType = Object.freeze({
19
+ THERMOSTAT: 'Thermostat',
20
+ TEMPSENSOR: 'TemperatureSensor',
21
+ SMOKESENSOR: 'Protect',
22
+ CAMERA: 'Camera',
23
+ DOORBELL: 'Doorbell',
24
+ FLOODLIGHT: 'FloodlightCamera',
25
+ WEATHER: 'Weather',
26
+ HEATLINK: 'Heatlink',
27
+ LOCK: 'Lock',
28
+ ALARM: 'Alarm',
29
+ });
30
+
31
+ async function loadDeviceModules(log, pluginDir = '') {
32
+ let baseDir = path.join(__dirname, pluginDir);
33
+ let deviceMap = new Map();
34
+ let files = (await fs.readdir(baseDir)).sort();
35
+
36
+ for (const file of files) {
37
+ if (file.endsWith('.js') === false) {
38
+ continue;
39
+ }
40
+
41
+ try {
42
+ let module = await import(url.pathToFileURL(path.join(baseDir, file)).href);
43
+ let exportsToCheck = Object.values(module);
44
+
45
+ for (const exported of exportsToCheck) {
46
+ if (typeof exported !== 'function') {
47
+ continue;
48
+ }
49
+
50
+ let proto = Object.getPrototypeOf(exported);
51
+ while (proto !== undefined && proto.name !== '') {
52
+ if (proto === HomeKitDevice) {
53
+ if (
54
+ typeof exported.TYPE !== 'string' ||
55
+ exported.TYPE === '' ||
56
+ typeof exported.VERSION !== 'string' ||
57
+ exported.VERSION === ''
58
+ ) {
59
+ log?.warn?.('Skipping device module %s (missing TYPE or VERSION)', file);
60
+ break;
61
+ }
62
+
63
+ if (deviceMap.has(exported.TYPE) === false) {
64
+ deviceMap.set(exported.TYPE, exported);
65
+ log?.info?.('Loaded device module "%s" (v%s)', exported.TYPE, exported.VERSION);
66
+ }
67
+
68
+ break;
69
+ }
70
+
71
+ proto = Object.getPrototypeOf(proto);
72
+ }
73
+ }
74
+ } catch (error) {
75
+ log?.warn?.('Failed to load device support file "%s": %s', file, error.message);
76
+ }
77
+ }
78
+
79
+ return deviceMap;
80
+ }
81
+
82
+ function getDeviceHKCategory(type) {
83
+ let category = 1; // Categories.OTHER
84
+
85
+ if (type === DeviceType.LOCK) {
86
+ category = 6; // Categories.DOOR_LOCK
87
+ }
88
+
89
+ if (type === DeviceType.THERMOSTAT) {
90
+ category = 9; // Categories.THERMOSTAT
91
+ }
92
+
93
+ if (type === DeviceType.TEMPSENSOR || type === DeviceType.HEATLINK || type === DeviceType.SMOKESENSOR || type === DeviceType.WEATHER) {
94
+ category = 10; // Categories.SENSOR
95
+ }
96
+
97
+ if (type === DeviceType.ALARM) {
98
+ category = 11; // Categories.SECURITY_SYSTEM
99
+ }
100
+
101
+ if (type === DeviceType.CAMERA || type === DeviceType.FLOODLIGHT) {
102
+ category = 17; // Categories.IP_CAMERA
103
+ }
104
+
105
+ if (type === DeviceType.DOORBELL) {
106
+ category = 18; // Categories.VIDEO_DOORBELL
107
+ }
108
+
109
+ return category;
110
+ }
111
+
112
+ // Define exports
113
+ export { DeviceType, loadDeviceModules, getDeviceHKCategory };
package/dist/index.js CHANGED
@@ -10,13 +10,14 @@
10
10
  // Nest Temp Sensors (1st gen)
11
11
  // Nest Cameras (Cam Indoor, IQ Indoor, Outdoor, IQ Outdoor, Cam with Floodlight)
12
12
  // Nest Doorbells (wired 1st gen)
13
+ // Nest HeatLink
13
14
  //
14
15
  // The accessory supports authentication to Nest/Google using either a Nest account OR Google (migrated Nest account) account.
15
16
  // "preliminary" support for using FieldTest account types also.
16
17
  //
17
18
  // Supports both Nest REST and Protobuf APIs for communication
18
19
  //
19
- // Code version 7/10/2024
20
+ // Code version 2025.06.05
20
21
  // Mark Hulskamp
21
22
  'use strict';
22
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 20/11/2024
8
+ // Code version 2025.06.10
9
9
  // Mark Hulskamp
10
10
  'use strict';
11
11
 
@@ -123,11 +123,11 @@ export default class NexusTalk extends Streamer {
123
123
  }
124
124
 
125
125
  this.connected = false; // Starting connection
126
- this?.log?.debug && this.log.debug('Connection started to "%s"', host);
126
+ this?.log?.debug?.('Connection started to "%s"', host);
127
127
 
128
128
  this.#socket = tls.connect({ host: host, port: 1443 }, () => {
129
129
  // Opened connection to Nexus server, so now need to authenticate ourselves
130
- this?.log?.debug && this.log.debug('Connection established to "%s"', host);
130
+ this?.log?.debug?.('Connection established to "%s"', host);
131
131
 
132
132
  this.#socket.setKeepAlive(true); // Keep socket connection alive
133
133
  this.host = host; // update internal host name since we've connected
@@ -144,7 +144,7 @@ export default class NexusTalk extends Streamer {
144
144
  });
145
145
 
146
146
  this.#socket.on('close', (hadError) => {
147
- this?.log?.debug && this.log.debug('Connection closed to "%s"', host);
147
+ this?.log?.debug?.('Connection closed to "%s"', host);
148
148
 
149
149
  clearInterval(this.pingTimer);
150
150
  clearTimeout(this.stalledTimer);
@@ -199,7 +199,7 @@ export default class NexusTalk extends Streamer {
199
199
 
200
200
  if (this.host !== deviceData.streaming_host) {
201
201
  this.host = deviceData.streaming_host;
202
- this?.log?.debug && this.log.debug('New host has been requested for connection. Host requested is "%s"', this.host);
202
+ this?.log?.debug?.('New host has been requested for connection. Host requested is "%s"', this.host);
203
203
  }
204
204
 
205
205
  // Let our parent handle the remaining updates
@@ -308,7 +308,7 @@ export default class NexusTalk extends Streamer {
308
308
 
309
309
  if (reauthorise === true && authoriseRequest !== null) {
310
310
  // Request to re-authorise only
311
- this?.log?.debug && this.log.debug('Re-authentication requested to "%s"', this.host);
311
+ this?.log?.debug?.('Re-authentication requested to "%s"', this.host);
312
312
  this.#sendMessage(PacketType.AUTHORIZE_REQUEST, authoriseRequest);
313
313
  }
314
314
 
@@ -316,7 +316,7 @@ export default class NexusTalk extends Streamer {
316
316
  // This isn't a re-authorise request, so perform 'Hello' packet
317
317
  let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.Hello');
318
318
  if (TraitMap !== null) {
319
- this?.log?.debug && this.log.debug('Performing authentication to "%s"', this.host);
319
+ this?.log?.debug?.('Performing authentication to "%s"', this.host);
320
320
 
321
321
  let encodedData = TraitMap.encode(
322
322
  TraitMap.fromObject({
@@ -350,7 +350,7 @@ export default class NexusTalk extends Streamer {
350
350
  return;
351
351
  }
352
352
 
353
- this?.log?.debug && this.log.debug('Redirect requested from "%s" to "%s"', this.host, redirectToHost);
353
+ this?.log?.debug?.('Redirect requested from "%s" to "%s"', this.host, redirectToHost);
354
354
 
355
355
  // Setup listener for socket close event. Once socket is closed, we'll perform the redirect
356
356
  this.#socket &&
@@ -389,7 +389,7 @@ export default class NexusTalk extends Streamer {
389
389
  this.#packets = [];
390
390
  this.#messages = [];
391
391
 
392
- this?.log?.debug && this.log.debug('Playback started from "%s" with session ID "%s"', this.host, this.#id);
392
+ this?.log?.debug?.('Playback started from "%s" with session ID "%s"', this.host, this.#id);
393
393
  }
394
394
  }
395
395
 
@@ -403,12 +403,11 @@ export default class NexusTalk extends Streamer {
403
403
  // <-- testing to see how often this occurs first
404
404
  clearTimeout(this.stalledTimer);
405
405
  this.stalledTimer = setTimeout(() => {
406
- this?.log?.debug &&
407
- this.log.debug(
408
- 'We have not received any data from nexus in the past "%s" seconds for uuid "%s". Attempting restart',
409
- 10,
410
- this.uuid,
411
- );
406
+ this?.log?.debug?.(
407
+ 'We have not received any data from nexus in the past "%s" seconds for uuid "%s". Attempting restart',
408
+ 10,
409
+ this.uuid,
410
+ );
412
411
 
413
412
  // Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
414
413
  this.#socket &&
@@ -437,13 +436,12 @@ export default class NexusTalk extends Streamer {
437
436
 
438
437
  if (this.#id !== undefined && decodedMessage.reason === 'USER_ENDED_SESSION') {
439
438
  // Normal playback ended ie: when we stopped playback
440
- this?.log?.debug && this.log.debug('Playback ended on "%s"', this.host);
439
+ this?.log?.debug?.('Playback ended on "%s"', this.host);
441
440
  }
442
441
 
443
442
  if (decodedMessage.reason !== 'USER_ENDED_SESSION') {
444
443
  // Error during playback, so we'll attempt to restart by reconnection to host
445
- this?.log?.debug &&
446
- this.log.debug('Playback ended on "%s" with error "%s". Attempting reconnection', this.host, decodedMessage.reason);
444
+ this?.log?.debug?.('Playback ended on "%s" with error "%s". Attempting reconnection', this.host, decodedMessage.reason);
447
445
 
448
446
  // Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
449
447
  this.#socket &&
@@ -464,7 +462,7 @@ export default class NexusTalk extends Streamer {
464
462
  this.#Authenticate(true); // Update authorisation only
465
463
  } else {
466
464
  // NexusStreamer Error, packet.message contains the message
467
- this?.log?.debug && this.log.debug('Error', decodedMessage.message);
465
+ this?.log?.debug?.('Error', decodedMessage.message);
468
466
  }
469
467
  }
470
468
  }
@@ -474,7 +472,7 @@ export default class NexusTalk extends Streamer {
474
472
  if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
475
473
  //let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackBegin').decode(payload).toJSON();
476
474
  this.audio.talking = true;
477
- this?.log?.debug && this.log.debug('Talking started on uuid "%s"', this.uuid);
475
+ this?.log?.debug?.('Talking started on uuid "%s"', this.uuid);
478
476
  }
479
477
  }
480
478
 
@@ -483,7 +481,7 @@ export default class NexusTalk extends Streamer {
483
481
  if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
484
482
  //let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackEnd').decode(payload).toJSON();
485
483
  this.audio.talking = false;
486
- this?.log?.debug && this.log.debug('Talking ended on uuid "%s"', this.uuid);
484
+ this?.log?.debug?.('Talking ended on uuid "%s"', this.uuid);
487
485
  }
488
486
  }
489
487