-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.js
4864 lines (4463 loc) · 223 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
*
* iobroker extron (SIS) Adapter
*
* Copyright (c) 2020-2024, Bannsaenger <bannsaenger@gmx.de>
*
* CC-NC-BY 4.0 License
*
* last edit 20241023 mschlgl
*/
// The adapter-core module gives you access to the core ioBroker functions
const utils = require('@iobroker/adapter-core');
// Load your modules here, e.g.:
// @ts-ignore
const fs = require('fs');
// @ts-ignore
const Client = require('ssh2').Client;
const Net = require('net');
// @ts-ignore
const ping = require ('net-ping');
// @ts-ignore
const path = require('path');
const errCodes = {
'E01' : 'Invalid input channel number (out of range)',
'E10' : 'Unrecognized command',
'E11' : 'Invalid preset number (out of range)',
'E12' : 'Invalid port/output number (out of range)',
'E13' : 'Invalid parameter (number is out of range)',
'E14' : 'Not valid for this configuration',
'E17' : 'Invalid command for signal type / system timed out',
'E18' : 'System/command timed out',
'E22' : 'Busy',
'E24' : 'Privilege violation',
'E25' : 'Device not present',
'E26' : 'Maximum number of connections exceeded',
'E27' : 'Invalid event number',
'E28' : 'Bad filename or file not found',
'E30' : 'Hardware failure',
'E31' : 'Attempt to break port passthrough when not set'
};
const invalidChars = ['+','~',',','@','=',"'",'[',']','{','}','<','>','`','"',':',';','|','\\','?'];
class Extron extends utils.Adapter {
/**
* @param {Partial<utils.AdapterOptions>} [options={}]
*/
constructor(options) {
super({
...options,
name: 'extron',
});
this.on('ready', this.onReady.bind(this));
this.on('stateChange', this.onStateChange.bind(this));
// this.on('objectChange', this.onObjectChange.bind(this));
this.on('message', this.onMessage.bind(this));
this.on('unload', this.onUnload.bind(this));
}
/**
* called to initialize internal variables
*/
initVars() {
this.log.debug('initVars(): Extron initializing internal variables');
this.sendBuffer = []; // Send buffer (Array of commands to send)
this.fileBuffer = new Uint8Array; // buffer for file data
this.grpCmdBuf = new Array(65).fill([]); // buffer for group command while a group deletion is pending
// Status variables
this.isDeviceChecked = false; // will be true if device sends banner and will be verified
this.isLoggedIn = false; // will be true once telnet login completed
this.isVerboseMode = false; // will be true if verbose mode 3 is active
this.initDone = false; // will be true if all init is done
this.versionSet = false; // will be true if the version is once set in the db
this.device = {'model':'','name':'','version':'','description':'','connectionState':'NEW','ipAddress':this.config.host,'port':this.config.port,'active':true}; // will be filled according to device responses
this.statusRequested = false; // will be true once device status has been requested after init
this.statusSent = false; // will be true once database settings have been sent to device
this.clientReady = false; // will be true if device connection is ready
//this.timers = {}; // Some timers and intervalls
this.debugSSH = false; // debug option for full ssh debug log on adapter.log.silly
this.client = new Client(); // Create a ssh lient socket to connect to the device
this.net = new Net.Socket({'readable':true,'writable' : true, 'allowHalfOpen' : false}); // Create a client socket to connect to the device
this.net.setKeepAlive(true);
this.net.setNoDelay(true);
this.stream = undefined; // placeholder for the stream
this.streamAvailable = true; // if false wait for continue event
this.stateList = []; // will be filled with all existing states
this.maxPollCount = typeof this.config.maxPollCount != 'undefined'?this.config.maxPollCount:10; // set maxPollCount if undefined set 10
this.pollCount = 0; // count sent status query
this.playerLoaded = [false, false, false, false, false, false, false,false]; // remember which player has a file assigned
this.auxOutEnabled = [false, false, false, false, false, false, false,false]; // remember which aux output is enabled
this.groupTypes = new Array(65); // prepare array to hold the type of groups
this.groupTypes.fill(0);
this.groupMembers = new Array(65); // prepare array to hold actual group members
this.groupMembers.fill([]);
this.grpDelPnd = new Array(65).fill(false); // prepare array to flag group deletions pending
this.fileSend = false; // flag to signal a file is currently sended
this.requestDir = false; // flag to signal a list user files command has been issued and a directory list is to be received
this.file = {'fileName' : '', 'timeStamp' : '', 'fileSize': 0}; // file object
this.fileList = {'freeSpace' : 0, 'files' : [this.file]}; // array to hold current file list
this.stateBuf = [{'id': '', 'timestamp' : 0}]; // array to hold state changes with timestamp
this.presetList = ''; // list of SMD202 preset channels
this.requestPresets = false; // flag to signal thet device preset list has been requested (applies to SMD202)
this.danteDevices = {}; // store subdevices controlled via DANTE
this.tmrRes = this.config.tmrRes || 100; // timer resolution, default 100 ms
this.preCheckWithICMP = this.config.preCheckWithICMP === undefined ? true : this.config.preCheckWithICMP, // check the availability of the device with ping
this.tryICMPAfterRetries = this.config.tryICMPAfterRetries || 2; // switch to ICMP (ping) availability check after n tries, -1 = off
this.connectTimeout = this.config.connectTimeout || 3000; // time to wait for connection to complet in ms (defalt 3s)
this.reConnectTimeout = this.config.reconnectDelay || 10000; // time to wait after a connection failure for a new attempt (default: 10 s)
// -------------------------------------------------------------------------------------
// create a ping session for connection checking
this.ping_options = {
networkProtocol: ping.NetworkProtocol.IPv4,
packetSize: 16,
retries: 1,
timeout: 1000,
ttl: 128
};
this.pingSession = ping.createSession(this.ping_options);
// ping session events
this.pingSession.on('close', this.onPingClose.bind(this));
this.pingSession.on('error', this.onPingError.bind(this));
// start central timer/interval handler
// @ts-ignore
this.centralIntervalTimer = setInterval(this.centralIntervalTimer.bind(this), this.tmrRes);
}
/**
* Intervaltimer tmrRes (default: 1) per Second to handle all timeouts and reconnets etc.
*/
centralIntervalTimer() {
try {
// iterate through all devices
//for (const device of this.devices) {
const device = this.device;
switch (device.connectionState) {
case 'NEW': // Initialize timers etc.
if (this.preCheckWithICMP) {
device.connectionState = 'ICMP_CHECKING';
device.timeToWait = this.connectTimeout/this.tmrRes;
this.pingSession.pingHost(device.ipAddress, this.onPingCallback.bind(this));
} else {
device.connectionState = 'CONNECTING';
device.timeToWait = this.connectTimeout/this.tmrRes;
device.connectionAttempt = this.tryICMPAfterRetries;
this.clientConnect();
}
break;
case 'CONNECTED': // Handle timers for Alive and GetStatus
if (device.timeoutPolling > 0) {
device.timeoutPolling--;
} else {
device.timeoutPolling = this.config.pollDelay/this.tmrRes;
this.queryStatus();
}
break;
case 'CLOSED':
case 'FAILED':
device.timeToWait = 0; // handle closed or failed connections immediately
device.connectionState = 'CONNECTING';
break;
case 'CONNECTING': // Check whether the connection timneout has exeeded
if (device.timeToWait > 0) {
device.timeToWait--;
} else {
//if (this.callback) this.callback('OFFLINE', {'message': `Device ${device.ipAddress} is offline`, 'macAddress': device.device, 'senderIp': device.ipAddress});
//if (device.net) device.net.destroy();
//device.net = undefined;
device.timeToWait = this.reConnectTimeout/this.tmrRes;
if (device.connectionAttempt > 0) {
device.connectionAttempt--;
//this.log.warn(`noch ${device.connectionAttempt} übrig`)
device.connectionState = 'RECONNECT_WAITING';
} else {
//this.log.warn(`gehe zu ICMP check`)
device.connectionState = 'ICMP_CHECKING';
}
}
break;
case 'RECONNECT_WAITING': // try to reconnect when timer expired
if (device.timeToWait > 0) {
device.timeToWait--;
} else {
device.connectionState = 'CONNECTING';
device.timeToWait = this.connectTimeout/this.tmrRes;
this.clientConnect();
}
break;
case 'ICMP_CHECKING':
if (device.timeToWait > 0) {
device.timeToWait--;
} else {
device.timeToWait = this.reConnectTimeout/this.tmrRes;
this.pingSession.pingHost(device.ipAddress, this.onPingCallback.bind(this));
}
break;
case 'ICMP_AVAILABLE':
device.connectionState = 'CONNECTING';
device.timeToWait = this.connectTimeout/this.tmrRes;
device.connectionAttempt = this.tryICMPAfterRetries;
this.clientConnect();
break;
}
//}
} catch(err) {
this.errorHandler(err, 'centralIntervalTimer');
}
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
try {
// Initialize your adapter here
const startTime = Date.now();
this.initVars();
// Reset the connection indicator during startup
this.setState('info.connection', false, true);
// read Objects template for object generation
this.objectTemplates = JSON.parse(fs.readFileSync(__dirname + '/lib/object_templates.json', 'utf8'));
// read devices for device check
this.devices = JSON.parse(fs.readFileSync(__dirname + '/lib/device_mapping.json', 'utf8'));
// Check whether the device type is already chosen. If not, skip the initialisation process and run in offline mode
// only for the messageBox
if (this.config.device === '') {
this.log.warn(`No device type specified. Running in Offline Mode`);
} else {
// The adapters config (in the instance object everything under the attribute "native") is accessible via
// this.config:
this.log.info('onReady(): configured host/port: ' + this.config.host + ':' + this.config.port);
/*
* For every state in the system there has to be also an object of type state
*/
await this.setInstanceNameAsync();
await this.createDatabaseAsync();
await this.createStatesListAsync();
// In order to get state updates, you need to subscribe to them. The following line adds a subscription for our variable we have created above.
// this.subscribeStates('testVariable');
this.subscribeStates('*'); // subscribe to all states
// Client callbacks
switch (this.config.type) {
case 'ssh' :
this.client.on('keyboard-interactive', this.onClientKeyboard.bind(this));
this.client.on('ready', this.onClientReady.bind(this));
//this.client.on('banner', this.onClientBanner.bind(this));
this.client.on('close', this.onClientClose.bind(this));
this.client.on('error', this.onClientError.bind(this));
this.client.on('end', this.onClientEnd.bind(this));
break;
case 'telnet' :
this.net.on('connectionAttempt',()=>{this.log.debug(`this.net.on: Telnet: connectionAttempt started`);});
this.net.on('connectionAttemptTimeout',()=>{this.log.warn(`this.net.on: Telnet: connectionAttemptTimeout`);this.device.connectionState = 'FAILED';});
this.net.on('connectionAttemptFailed',()=>{this.log.warn(`this.net.on: Telnet: connectionAttemptFailed`);this.device.connectionState = 'FAILED';});
this.net.on('timeout',()=>{this.log.warn(`this.net.on: Telnet: connection idle timeout`);});
this.net.on('connect',()=>{this.log.debug(`this.net.on: Telnet: connected`);});
this.net.on('ready', this.onClientReady.bind(this));
this.net.on('error', this.onClientError.bind(this));
this.net.on('end', this.onClientEnd.bind(this));
this.net.on('close', ()=>{
this.log.debug(`this.net.on: Telnet: socket closed`);
this.device.connectionState = 'CLOSED';
//this.clientReConnect();
});
break;
}
this.log.info(`onReady(): Extron took ${Date.now() - startTime}ms to initialize and setup db`);
//this.clientConnect();
}
} catch (err) {
this.errorHandler(err, 'onReady');
}
}
/**
* try to connect to the device
*/
clientConnect() {
try {
this.log.info(`clientConnect(): Extron connecting via ${this.config.type} to: ${this.config.host}:${this.config.port}`);
switch (this.config.type) {
case 'ssh' :
this.client.connect({
'host': this.config.host,
'port': Number(this.config.port),
'username': this.config.user,
'password': this.config.pass,
'keepaliveInterval': 5000,
// @ts-ignore
'debug': this.debugSSH ? this.log.silly.bind(this) : undefined,
//'debug': true,
'readyTimeout': 5000,
'tryKeyboard': true
});
break;
case 'telnet' :
this.stream = this.net.connect(Number(this.config.port), this.config.host);
break;
}
} catch (err) {
this.errorHandler(err, 'clientConnect');
}
}
/**
* reconnect Client after error
*/
clientReConnect() {
// clear poll timer
//clearTimeout(this.timers.timeoutQueryStatus); // stop the query timer
// Status variables to be reset
this.setState('info.connection', false, true);
this.isLoggedIn = false; // will be true once telnet login completed
this.isVerboseMode = false; // will be true if verbose mode 3 is active
this.isDeviceChecked = false; // will be true if device sends banner and will be verified
this.log.info(`clientReConnect(): reconnecting after ${this.config.reconnectDelay}ms`);
//this.timers.timeoutReconnectClient = setTimeout(this.clientConnect.bind(this),this.config.reconnectDelay);
}
/**
* called if keyboard authentication must be fullfilled
* @param {string} _name
* @param {string} _instructions
* @param {string} _instructionsLang
* @param {array} _prompts
* @param {function} finish
*/
onClientKeyboard(_name, _instructions, _instructionsLang, _prompts, finish) {
try {
this.log.info('onClientKeyboard(): Extron keyboard autentication in progress. Send back password');
finish([this.config.pass]);
} catch (err) {
this.errorHandler(err, 'onClientKeyboard');
}
}
/**
* called if client is successfully connected
*/
onClientReady() {
try {
switch (this.config.type) {
case 'ssh' :
this.log.info('onClientReady(): Extron is authenticated successfully, now open the stream');
this.client.shell(function (error, channel) {
try {
if (error) throw error;
// @ts-ignore
this.log.info('onClientReady(): Extron shell established channel');
this.stream = channel;
} catch (err) {
// @ts-ignore
this.errorHandler(err, 'onClientReady');
}
});
break;
case 'telnet' :
this.log.info('onClientReady(): Extron established Telnet connection');
break;
}
this.stream.on('error', this.onStreamError.bind(this));
// @ts-ignore
this.stream.on('close', this.onStreamClose.bind(this));
// @ts-ignore
this.stream.on('data', this.onStreamData.bind(this));
// @ts-ignore
this.stream.on('drain', this.onStreamContinue.bind(this));
// Set the connection indicator after authentication and an open stream
// @ts-ignore
this.log.info('onClientReady(): Extron connected');
this.device.connectionState = 'CONNECTED';
this.device.timeToWait = 0;
this.device.timeoutPolling = 0; // next timercycle there will be a polling
// @ts-ignore
// this.setState('info.connection', true, true);
//this.timers.timeoutQueryStatus = setTimeout(this.queryStatus.bind(this), this.config.pollDelay); // start polling the device
} catch (err) {
this.errorHandler(err, 'onClientReady');
}
}
/**
* called if client has recieved a banner
* @param {string} message
* @param {string} language
*/
/*
onClientBanner(message, language) {
this.log.info(`onClientBanner(): Extron sent back banner: "${message}" in language: "${language}"`);
}*/
/**
* called if client is closed
*/
onClientClose() {
try {
this.log.info('onClientClose(): Extron SSH client closed');
// Reset the connection indicator
this.setState('info.connection', false, true);
this.clientReady = false;
this.isDeviceChecked = false; // will be true if device sends banner and will be verified
this.isVerboseMode = false; // will be true if verbose mode 3 is active
this.initDone = false; // will be true if all init is done
this.versionSet = false; // will be true if the version is once set in the db
this.statusRequested = false; // will be true if device status has been requested after init
this.statusSent = false; // will be true once database settings have been sent to device
this.stream = undefined;
this.device.connectionState = 'CLOSED';
} catch (err) {
this.errorHandler(err, 'onClientClose');
}
}
/**
* called if the socket is disconnected
*/
onClientEnd() {
try {
this.log.info('onClientEnd(): Extron client socket got disconnected');
this.setState('info.connection', false, true);
this.device.connectionState = 'CLOSED';
} catch (err) {
this.errorHandler(err, 'onClientEnd');
}
}
/**
* called if client receives an error
* @param {any} err
*/
onClientError(err) {
switch (this.config.type) {
case 'ssh' :
break;
case 'telnet' :
if (this.net.connect) {
this.log.debug(`onClientError(): telnet connection pending ...`);
return;
}
break;
}
this.device.connectionState = 'FAILED';
this.log.error(`onClientError(): error detected ${err}`);
this.errorHandler(err, 'onClientError');
}
/**
* Is called if a session error occurs
* @param {any} err Error
*/
onPingError(err) {
try {
this.log.error(`ICMP session Server got Error: <${err.toString()}> closing server.`);
this.pingSession.close();
} catch(err) {
this.errorHandler(err, 'onPingError');
}
}
/**
* Is called when the session is closed via session.close
*/
onPingClose() {
this.log.info('onPingClose(): ICMP session is closed');
}
/**
* Is used as callback for session.ping
* @param {Error} error Instance of the Error class or a sub-class, or null if no error occurred
* @param {any} target The target parameter as specified in the request still be the target host and NOT the responding gateway
* @param {Date} sent An instance of the Date class specifying when the first ping was sent for this request (refer to the Round Trip Time section for more information)
* @param {Date} rcvd An instance of the Date class specifying when the request completed (refer to the Round Trip Time section for more information)
*/
onPingCallback(error, target, sent, rcvd) {
try {
// @ts-ignore
const ms = rcvd - sent;
if (error)
this.log.debug(`ping: ${target}: ${error.toString()}`);
else {
this.log.debug(`ping: ${target}: Alive (ms=${ms})`);
//const device = this.devices.find(item => item.ipAddress === target);
this.device.connectionState = 'ICMP_AVAILABLE';
// reconnect is done in the timer routine
}
} catch(err) {
this.errorHandler(err, 'onPingCallback');
}
}
/**
* called to send data to the stream
* @param {string | Uint8Array | any} data
* @param {string | void} device
*/
streamSend(data, device = '') {
try {
if (device !== '') if ((this.devices[this.config.device].model.includes('Plus')||this.devices[this.config.device].model.includes('XMP') )) { // DANTE control only on DMP plus / XMP series
data = data.replace('\r','|'); // for DANTE relayed messages replace '\r' with '|'
data = `{dante@${device}:${data}}\r`; // format DANTe relay message
}
if (this.streamAvailable) {
this.setState('info.connection', true, true);
if (!this.fileSend) this.log.debug(`streamSend(): Extron sends data to the ${this.config.type} stream: "${this.decodeBufferToLog(data)}"`);
this.streamAvailable = this.stream.write(data);
/*switch (this.config.type) {
case 'ssh' :
this.streamAvailable = this.stream.write(data);
break;
case 'telnet' :
this.streamAvailable = this.net.write(data);
break;
}*/
} else {
this.setState('info.connection', false, true);
if (!this.fileSend) {
const bufSize = this.sendBuffer.push(data);
this.log.warn(`streamSend(): Extron push data to the send buffer: "${this.decodeBufferToLog(data)}" new buffersize:${bufSize}`);
} else {
const bufSize = Array.from(this.fileBuffer).push(data);
this.log.warn(`streamSend(): Extron push data to the file buffer: new buffersize:${bufSize}`);
}
}
} catch (err) {
this.errorHandler(err, 'streamSend');
this.device.connectionState = 'FAILED';
//this.clientReConnect();
}
}
/**
* called if stream receives data
* @param {string | Uint8Array} data
*/
async onStreamData(data) {
let members = [];
const userFileList = [];
const device = this.devices[this.config.device].short;
this.streamAvailable = true; // if we receive data the stream is available
if (this.fileSend) return; // do nothing during file transmission
try {
this.log.debug(`onStreamData(): received data: "${this.decodeBufferToLog(data)}"`);
data = data.toString(); // convert buffer to String
if (!this.isDeviceChecked) { // the first data has to be the banner with device info
if (data.includes(this.devices[this.config.device].pno)) {
this.isDeviceChecked = true;
this.log.info(`onStreamData(): Device "${this.devices[this.config.device].model}" verified`);
// this.setState('info.connection', true, true);
if (this.config.type === 'ssh') {
if (!this.isVerboseMode) { // enter the verbose mode
this.sendVerboseMode();
return;
}
}
} else {
throw { 'message': 'Device mismatch error', 'stack' : 'Please recreate the instance or connect to the correct device' };
}
return;
}
if (this.config.type === 'telnet') {
//if (!this.isLoggedIn) {
if (data.toString().includes('Password:')) {
this.isLoggedIn = false;
this.isVerboseMode = false;
this.setState('info.connection', false, true);
this.log.info('onStreamData(): Extron received Telnet Password request');
this.streamSend(`${this.config.pass}\r`);
return;
}
if (data.toString().includes('Login Administrator')) {
this.isLoggedIn = true;
this.log.info('onStreamData(): Extron Telnet logged in');
this.setState('info.connection', true, true);
if (!this.isVerboseMode) { // enter the verbose mode
this.sendVerboseMode();
return;
}
return;
}
//}
}
// check for DANTE control / setup messages returning a list of DANTE device names separated by \r\n
if (data.match(/(Expr[al]) ([\w-]*[\s\S]*)\r\n/im)) {
data = data.replace(/[\n]/gm,'*'); // change device list separator from "\n" to "*"
data = data.replace('*\r*', '\r\n'); // restore end of message
this.log.debug(`onStreamData(): received DANTE control message: "${this.decodeBufferToLog(data)}"`);
}
// iterate through multiple answers connected via [LF]
for (const cmdPart of data.split('\n')) {
const answer = cmdPart.replace(/[\r\n]/gm, ''); // remove [CR] and [LF] from string
/** Error handling
if (answer.match(/^E\d\d/gim)) { // received an error
throw { 'message': 'Error response from device', 'stack' : errCodes[answer] };
}**/
if (this.requestDir && answer.match(/\.\w{3} /)) {
this.log.info(`onStreamData(): received file data: "${answer}"`);
userFileList.push(answer);
} else if (this.requestDir && answer.includes('Bytes Left')) {
this.log.info(`onStreamData(): received freespace: "${answer.match(/\d+/)}"`);
userFileList.push(answer);
this.requestDir = false; // directory list has been received, clear flag
this.fileList.freeSpace = Number(answer.match(/\d+/)); // set freeSpace in list
this.setUserFilesAsync(userFileList); // call subroutine to set database values
} else if (this.requestPresets) {
this.presetList += answer;
if (answer.match(/"name":".*"\}\]$/)) {
this.log.debug(`onStreamData: end of presetList detected`);
this.requestPresets = false;
this.presetList = this.presetList.match(/(?!TvprG)(\[\{".*"\}\])/)[0];
this.setPresets(this.presetList);
}
} else
{ // lookup the command
// const matchArray = answer.match(/([A-Z][a-z]+[A-Z]|\w{3})(\d*)\*?,? ?(.*)/i); // initial separation detecting eg. "DsG60000*-10"
const matchArray = answer.match(/({(dante)@(.*)})?([A-Z][a-z]{1,3}[A-Z]|E|\w{3})([\w-]*|\d*),?\*? ?(.*)/i); // extended to detect DANTE remote responses eg. "{dante@AXI44-92efe7}DsG60000*-10"
if (matchArray) { // if any match
//this.log.debug(`onStreamData(): ${matchArray}`);
const command = matchArray[4].toUpperCase();
const dante = matchArray[2]?(matchArray[2] == 'dante'): false;
const danteDevice = matchArray[3];
const ext1 = matchArray[5] ? matchArray[5] : '';
const ext2 = matchArray[6] ? matchArray[6] : '';
this.log.debug(`onStreamData(): ${dante?'"'+danteDevice+'" ':''}command "${command}", ext1 "${ext1}", ext2 "${ext2}`);
this.pollCount = 0; // reset pollcounter as valid data has been received
//this.timers.timeoutQueryStatus.refresh(); // refresh poll timer
switch (command) {
case 'E':
// Error handling
this.log.warn(`onStreamData(): Error response from ${dante?'danteDevice '+danteDevice:'device'} '${command}${ext1}': ${errCodes[command+ext1]}}`);
break;
case 'VRB':
this.log.info(`onStreamData(): ${dante?'danteDevice '+danteDevice:'device'} entered verbose mode: "${ext1}"`);
if (dante) this.setVerboseMode(danteDevice);
else this.setVerboseMode();
break;
case 'VER': // received a Version (answer to status query)
switch (ext1) {
case '00' :
this.log.debug(`onStreamData(): received ${dante?'"'+danteDevice+'" ':''}detailed firmware version: "${ext2}"`);
break;
case '01' :
this.log.debug(`onStreamData(): received ${dante?'"'+danteDevice+'" ':''}firmware version: "${ext2}"`);
if (!this.versionSet) {
if (!dante) this.versionSet = true;
if (!dante) this.device.version = `${ext2}`;
this.setState(`${dante?'dante.'+danteDevice:'device'}.version`, ext2, true);
this.log.info(`onStreamData(): set ${dante?'dante.'+danteDevice:'device'}.version: "${ext2}"`);
}
break;
case '02' :
this.log.debug(`onStreamData(): received ${dante?'"'+danteDevice+'" ':''}device description: "${ext2}"`);
break;
case '03' :
this.log.debug(`onStreamData(): received ${dante?'"'+danteDevice+'" ':''}device memory usage: "${ext2}"`);
break;
case '04' :
this.log.debug(`onStreamData(): received ${dante?'"'+danteDevice+'" ':''}user memory usage: "${ext2}"`);
break;
case '14' :
this.log.debug(`onStreamData(): received ${dante?'"'+danteDevice+'" ':''}embedded OS type and version: "${ext2}"`);
break;
case '20' :
this.log.debug(`onStreamData(): received ${dante?'"'+danteDevice+'" ':''}firmware version with build: "${ext2}"`);
break;
default:
this.log.warn(`onStreamData(): received ${dante?'"'+danteDevice+'" ':''}unknown version information: "${ext2}"`);
}
break;
case 'INF':
switch (ext1) {
case '01' :
this.log.info(`onStreamData(): received ${dante?'dante.'+danteDevice:'device'} model: "${ext2}"`);
if (!dante) this.device.model = `${ext2}`;
this.setState(`${dante?'dante.'+danteDevice:'device'}.model`, ext2, true);
break;
case '02' :
this.log.info(`onStreamData(): received ${dante?'dante.'+danteDevice:'device'} model description: "${ext2}"`);
if (!dante) this.device.description = `${ext2}`;
this.setState(`${dante?'dante.'+danteDevice:'device'}.description`, ext2, true);
break;
default:
this.log.warn(`onStreamData(): received ${dante?'"'+danteDevice+'" ':''}unknown information: "${ext2}"`);
}
break;
case 'IPN': // received a device name
this.log.info(`onStreamData(): received ${dante?'dante ':''}devicename: "${ext2}"`);
if (!dante) this.device.name = `${ext2}`;
this.setState(`${dante?'dante.'+danteDevice:'device'}.name`, ext2, true);
break;
case 'PNO': // received Part Number
this.log.info(`onStreamData(): received ${dante?`dante ${danteDevice} `:'device '}partnumber: "${ext1}"`);
this.setDevicePartnumber(danteDevice, ext1);
break;
// DSP SIS commands
case 'DSA' : // dynamics attack
this.log.info(`onStreamData(): received attack time change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
break;
case 'DSB':
if (ext1.startsWith('460')) { // AEC Block
this.log.info(`onStreamData(): received AEC config change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
} else
this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSC':
if (ext1.startsWith('460')) { // AEC Block
this.log.info(`onStreamData(): received AEC config change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
} else
this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSD':
if (ext1.startsWith('400')) { // input source control
this.log.info(`onStreamData(): received source ${command} from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: "${ext2}"`);
this.setSource(ext1, ext2);
} else if (ext1.startsWith('450')) { // input delay value change
this.log.info(`onStreamData(): received delay value change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}samples"`);
} else if (ext1.startsWith('590')) { // input automixer group change
this.log.info(`onStreamData(): received automix group change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('600')) { // aux output target change
this.log.info(`onStreamData(): received aux output target change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: "${ext2}"`);
this.setSource(ext1, ext2);
} else if (ext1.startsWith('650')) { // output delay value change
this.log.info(`onStreamData(): received delay value change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}samples"`);
} else
this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSE' : // DSP block bypass change
if (ext1.startsWith('41')) { // input filter block
this.log.info(`onStreamData(): received input filter block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('44')) { // input dynamics block
this.log.info(`onStreamData(): received input dynamics block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('450')) { // input delay block
this.log.info(`onStreamData(): received input delay block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('460')) { // input aec block
this.log.info(`onStreamData(): received input aec block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('480')) { // input ducker block
this.log.info(`onStreamData(): received input ducker block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('51')) { // output filter block
this.log.info(`onStreamData(): received output filter block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('52')) { // output feedback suppressor filter block
this.log.info(`onStreamData(): received output feedback suppressor filter block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('530')) { // output feedback suppressor block
this.log.info(`onStreamData(): received output feedback suppressor block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('540')) { // output dynamics filter block
this.log.info(`onStreamData(): received output dynamics block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('550')) { // output delay block
this.log.info(`onStreamData(): received output delay block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDspBlockStatus(ext1, ext2);
} else if (ext1.startsWith('56')) { // input ducker source block
this.log.info(`onStreamData(): received input ducker source enabled change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('57')) { // input ducker source block
this.log.info(`onStreamData(): received input ducker source enabled block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('590')) { // input automix block
this.log.info(`onStreamData(): received input automix block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('61')) { // output filter block
this.log.info(`onStreamData(): received output filter block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('640')) { // output dynamics block
this.log.info(`onStreamData(): received output dynamics block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('650')) { // output delay block
this.log.info(`onStreamData(): received output delay block bypass change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else
this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSF' : // filter frequency change
this.log.info(`onStreamData(): received filter frequency change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
break;
case 'DSG': // received a gain level change
this.log.info(`onStreamData(): received mute/gain ${command} from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
this.setGain(command, ext1, ext2);
break;
case 'DSH' :
if (ext1.startsWith('400')) { // digital input gain level change
this.log.info(`onStreamData(): received gain ${command} from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
this.setGain(command, ext1, ext2);
} else if (ext1.startsWith('440')) { // input dynamics hold time change
this.log.info(`onStreamData(): received hold time change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
} else if (ext1.startsWith('540')) { // input dynamics hold time change
this.log.info(`onStreamData(): received hold time change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
} else if (ext1.startsWith('640')) { // output dynamics hold time change
this.log.info(`onStreamData(): received hold time change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
} else
this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSJ' :
if (ext1.startsWith('400')) { // input config change
this.log.info(`onStreamData(): received input config change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
} else if (ext1.startsWith('590')) { // input DSP config change
this.log.info(`onStreamData(): received DSP block config change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
} else if (ext1.startsWith('600')) { // output config change
this.log.info(`onStreamData(): received output config change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
} else
this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSK' : // dynamics knee
this.log.info(`onStreamData(): received dynamic knee change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
break;
case 'DSL' : // dynamics release
this.log.info(`onStreamData(): received release time change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
break;
case 'DSM': // received a mute command
this.log.info(`onStreamData(): received mute ${command} from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
this.setGain(command, ext1, ext2);
break;
case 'DSN':
if (ext1.startsWith('460')) { // digital input AEC config change
this.log.info(`onStreamData(): received input AEC config change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
} else if (ext1.startsWith('590')) { // input automix config change
this.log.info(`onStreamData(): received input automix config change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
} else this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSO' : // filter slope 0=6dB/O, 1=12dB/O ... 7=48dB/O
this.log.info(`onStreamData(): received filter slope change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${6+(Number(ext2)*6)}dB/O"`);
break;
case 'DSP':
if (ext1.startsWith('2')) { // mixpoint automixer status change
this.log.info(`onStreamData(): received mixpoint automixer status change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
} else if (ext1.startsWith('400')) { // input polarity change
this.log.info(`onStreamData(): received input polarity change change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('590')) { // input automixer last mic change
this.log.info(`onStreamData(): received automix last mic change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('600')) { // output polarity change
this.log.info(`onStreamData(): received output polarity change change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: "${ext2}"`);
} else this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSQ' : // filter q-factor change
this.log.info(`onStreamData(): received filter Q-factor change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${Number(ext2)/1000}"`);
break;
case 'DSR' : // dynamics ratio
this.log.info(`onStreamData(): received ratio change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
break;
case 'DST' :
if (ext1.startsWith('44')) { // input dynamics threshold change
this.log.info(`onStreamData(): received a threshold change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
this.setDynamicsThreshold(ext1, ext2);
} else if (ext1.startsWith('450')) { // input delay reference temperature change
this.log.info(`onStreamData(): received a delay ref temperature change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}°F"`);
} else if (ext1.startsWith('480')) { // input dynamics threshold change
this.log.info(`onStreamData(): received a threshold change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}°F"`);
} else if (ext1.startsWith('540')) { // input dynamics threshold change
this.log.info(`onStreamData(): received a threshold change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}°F"`);
} else if (ext1.startsWith('550')) { // input delay reference temperature change
this.log.info(`onStreamData(): received a delay ref temperature change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}°F"`);
} else if (ext1.startsWith('590')) { // input dynamics threshold change
this.log.info(`onStreamData(): received a threshold change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}°F"`);
} else if (ext1.startsWith('640')) { // output dynamics block
this.log.info(`onStreamData(): received output dynamics threshold change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('650')) { // output delay reference temperature change
this.log.info(`onStreamData(): received a delay ref temperature change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}°F"`);
} else
this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSU' : // delay unit change 0=samples, 1=ms, 2=fuß, 3=m
this.log.info(`onStreamData(): received delay unit change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
break;
case 'DSV' : // unsolicited volume/meter level
break;
case 'DSW' : // AGC target window
if (ext1.startsWith('44')) { // input dynamics block
this.log.info(`onStreamData(): received input AGC window change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('540')) { // virtual return dynamics block
this.log.info(`onStreamData(): received virtual return AGC window change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else if (ext1.startsWith('640')) { // output dynamics block
this.log.info(`onStreamData(): received output AGC window change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value: "${ext2}"`);
} else
this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
case 'DSY' : // DSP block type change
// dyn 0=no, 1=cmp, 2=lim, 3=gate, 4=agc
// filter 0=no, 1= HP Butterworth, 2 = LP Butterworth, 3= Bass/Treble, 4= pra. EQ, 5= notvh EQ, 6=HP Bessel, 7= LP Bessel, 8= HP Linkwitz, 9= LP Linkwitz, 10 = Loudness
this.log.info(`onStreamData(): received DSP block type change from ${dante?'"'+danteDevice+'" ':''}OID : "${ext1}" value "${ext2}"`);
this.setDspBlockType(ext1, ext2);
break;
case 'DSZ':
if (ext1.startsWith('2')) { // mixPoint processing bypass change
this.log.info(`onStreamData(): processing bypass status change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
} else if (ext1.startsWith('400')) { // input phantom power change
this.log.info(`onStreamData(): Phantom power status change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
} else if (ext1.startsWith('590')) { // input automixer status change
this.log.info(`onStreamData(): automixer status change from ${dante?'"'+danteDevice+'" ':''}OID: "${ext1}" value: ${ext2}`);
} else
this.log.warn(`onStreamData(): unknown OID: "${ext1}" for ${dante?'"'+danteDevice+'" ':''}command: "${command}"`);
break;
// player commands
case 'PLAY': //received a play mode command
this.log.info(`onStreamData(): received play mode ${command} for Player: "${ext1}" value: "${ext2}"`);
this.setPlayMode(ext1, ext2);
break;
case 'CPLYA': //received a file association to a player
this.log.info(`onStreamData(): received filename for Player: "${ext1}" value: "${ext2}"`);
this.setFileName(ext1, ext2);
break;
case 'CPLYM': //received a set repeat mode command
this.log.info(`onStreamData(): received repeat mode ${command} for Player: "${ext1}" value: "${ext2}"`);
this.setRepeatMode(ext1, ext2);
break;
case 'IN1': // received a tie command from CrossPoint
case 'IN2':
case 'IN3':
case 'IN4':
case 'IN5':
case 'IN6':
case 'IN7':
case 'IN8':
this.log.info(`onStreamData(): received tie command ${command} for output: ${ext2}`);
this.setTie(command, ext2);
break;
case 'LOUT': // received a tie command for loop out
this.log.info(`onStreamData(): received tie command input "${ext1}" to loop output`);
this.setState(`connections.3.tie`, Number(ext1), true);
break;
case 'VMT': // received a video mute
this.log.info(`onStreamData(): received video mute for output "${ext1}" value "${ext2}"`);
if (device === 'sme211') this.setState(`connections.1.mute`, Number(ext1), true);
else this.setState(`connections.${ext1}.mute`, Number(ext2), true);
break;
// Begin SMD202 specific commands
case 'PLYRS' : // received video playing
this.log.info(`onStreamData(): received video playmode for player "${ext1}" value "${ext2}"`);