hb-zp-tools 0.0.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/lib/ZpTool.js ADDED
@@ -0,0 +1,1262 @@
1
+ // hb-zp-tools/lib/ZpTool.js
2
+ // Copyright © 2019-2024 Erik Baauw. All rights reserved.
3
+ //
4
+ // Command line interface to Sonos ZonePlayer.
5
+
6
+ import { CommandLineParser } from 'hb-lib-tools/CommandLineParser'
7
+ import { CommandLineTool } from 'hb-lib-tools/CommandLineTool'
8
+ import { JsonFormatter } from 'hb-lib-tools/JsonFormatter'
9
+ import { OptionParser } from 'hb-lib-tools/OptionParser'
10
+
11
+ import { ZpClient } from './ZpClient.js'
12
+ import { ZpListener } from './ZpListener.js'
13
+
14
+ const { b, u } = CommandLineTool
15
+ const { UsageError } = CommandLineParser
16
+
17
+ const usage = {
18
+ zp: `${b('zp')} [${b('-hVD')}] [${b('-H')} ${u('hostname')}[${b(':')}${u('port')}]] [${b('-t')} ${u('timeout')}] ${u('command')} [${u('argument')} ...]`,
19
+ info: `${b('info')} [${b('-hnv')}]`,
20
+ description: `${b('description')} [${b('-hnSs')}]`,
21
+ topology: `${b('topology')} [${b('-hn')}] [${b('-pv')}|${b('-r')}]`,
22
+ eventlog: `${b('eventlog')} [${b('-hnst')}]`,
23
+ browse: `${b('browse')} [${b('-hn')}] [${u('object')}]`,
24
+ play: `${b('play')} [${b('-h')}] [${u('uri')} [${u('meta')}]]`,
25
+ queue: `${b('queue')} [${b('-h')}] ${u('uri')} [${u('meta')}]`,
26
+ pause: `${b('pause')} [${b('-h')}]`,
27
+ stop: `${b('stop')} [${b('-h')}]`,
28
+ next: `${b('next')} [${b('-h')}]`,
29
+ previous: `${b('previous')} [${b('-h')}]`,
30
+ crossfade: `${b('crossfade')} [${b('-h')}] [${b('on')}|${b('off')}]`,
31
+ repeat: `${b('repeat')} [${b('-h')}] [${b('on')}|${b('1')}|${b('off')}]`,
32
+ shuffle: `${b('shuffle')} [${b('-h')}] [${b('on')}|${b('off')}]`,
33
+ sleepTimer: `${b('sleepTimer')} [${b('-h')}] [${u('time')}|${b('off')}]`,
34
+ groupVolume: `${b('groupVolume')} [${b('-h')}] [${u('volume')}]`,
35
+ groupMute: `${b('groupMute')} [${b('-h')}] [${b('on')}|${b('off')}]`,
36
+ join: `${b('join')} [${b('-h')}] ${u('zone')}`,
37
+ leave: `${b('leave')} [${b('-h')}]`,
38
+ volume: `${b('volume')} [${b('-h')}] [${u('volume')}]`,
39
+ mute: `${b('mute')} [${b('-h')}] [${b('on')}|${b('off')}]`,
40
+ bass: `${b('bass')} [${b('-h')}] [${b('--')}] [${u('bass')}]`,
41
+ treble: `${b('treble')} [${b('-h')}] [${b('--')}] [${u('treble')}]`,
42
+ loudness: `${b('loudness')} [${b('-h')}] [${b('on')}|${b('off')}]`,
43
+ balance: `${b('balance')} [${b('-h')}] [${b('--')}] [${u('balance')}]`,
44
+ nightSound: `${b('nightSound')} [${b('-h')}] [${b('on')}|${b('off')}]`,
45
+ speechEnhancement: `${b('speechEnhancement')} [${b('-h')}] [${b('on')}|${b('off')}]`,
46
+ surroundEnable: `${b('surroundEnable')} [${b('-h')}] [${b('on')}|${b('off')}]`,
47
+ tvLevel: `${b('tvLevel')} [${b('-h')}] [${b('--')}] [${u('level')}]`,
48
+ musicLevel: `${b('musicLevel')} [${b('-h')}] [${b('--')}] [${u('level')}]`,
49
+ musicPlaybackFull: `${b('musicPlaybackFull')} [${b('-h')}] [${b('on')}|${b('off')}]`,
50
+ heightLevel: `${b('heightLevel')} [${b('-h')}] [${b('--')}] [${u('level')}]`,
51
+ subEnable: `${b('subEnable')} [${b('-h')}] [${b('on')}|${b('off')}]`,
52
+ subLevel: `${b('subLevel')} [${b('-h')}] [${b('--')}] [${u('level')}]`,
53
+ led: `${b('led')} [${b('-h')}] [${b('on')}|${b('off')}]`,
54
+ buttonLock: `${b('buttonLock')} [${b('-h')}] [${b('on')}|${b('off')}]`
55
+ }
56
+
57
+ const description = {
58
+ zp: 'Command line interface to Sonos ZonePlayer.',
59
+ info: 'Print zone player properties.',
60
+ description: 'Print zone player device description.',
61
+ topology: 'Print zones and zone players known by the zone player.',
62
+ eventlog: 'Log zone player events.',
63
+ browse: 'Browse media.',
64
+ play: 'Play.',
65
+ queue: 'Queue.',
66
+ pause: 'Pause.',
67
+ stop: 'Stop.',
68
+ next: 'Go to next track.',
69
+ previous: 'Go to previous track.',
70
+ crossfade: 'Get/set/clear crossfade.',
71
+ repeat: 'Get/set repeat.',
72
+ shuffle: 'Get/set/clear shuffle.',
73
+ sleepTimer: 'Get/set/clear sleep timer.',
74
+ groupVolume: 'Get/set group volume.',
75
+ groupMute: 'Get/set/clear group mute.',
76
+ join: 'Join ZoneGroup.',
77
+ leave: 'Leave ZoneGroup.',
78
+ volume: 'Get/set volume.',
79
+ mute: 'Get/set/clear mute.',
80
+ bass: 'Get/set bass.',
81
+ treble: 'Get/set treble.',
82
+ loudness: 'Get/set/clear loudness.',
83
+ balance: 'Get/set balance.',
84
+ nightSound: 'Get/set/clear nightsound.',
85
+ speechEnhancement: 'Get/set/clear speech enhancement.',
86
+ surroundEnable: 'Get/set/clear surround enabled state.',
87
+ tvLevel: 'Get/set TV surround level.',
88
+ musicLevel: 'Get/set music surround level.',
89
+ musicPlaybackFull: 'Get/set/clear full music playback.',
90
+ heightLevel: 'Get/set height channel level.',
91
+ subEnable: 'Get/set/clear Sub enabled state.',
92
+ subLevel: 'Get/set Sub level.',
93
+ led: 'Get/set/clear LED state.',
94
+ buttonLock: 'Get/set/clear button lock state.'
95
+ }
96
+
97
+ const help = {
98
+ zp: `${description.zp}
99
+
100
+ Usage: ${usage.zp}
101
+
102
+ Parameters:
103
+ ${b('-h')}, ${b('--help')}
104
+ Print this help and exit.
105
+
106
+ ${b('-V')}, ${b('--version')}
107
+ Print version and exit.
108
+
109
+ ${b('-D')}, ${b('--debug')}
110
+ Print debug messages.
111
+
112
+ ${b('-H')} ${u('hostname')}[${b(':')}${u('port')}], ${b('--host=')}${u('hostname')}[${b(':')}${u('port')}]
113
+ Connect to ZonePlayer at ${u('hostname')}${b(':1400')} or ${u('hostname')}${b(':')}${u('port')}.
114
+ Default ZonePlayer can be set in the ${b('ZP_HOST')} environment variable.
115
+
116
+ ${b('-t')} ${u('timeout')}, ${b('--timeout=')}${u('timeout')}
117
+ Set timeout to ${u('timeout')} seconds instead of default ${b('5')}.
118
+
119
+ Commands:
120
+ ${usage.info}
121
+ ${description.info}
122
+
123
+ ${usage.description}
124
+ ${description.description}
125
+
126
+ ${usage.topology}
127
+ ${description.topology}
128
+
129
+ ${usage.eventlog}
130
+ ${description.eventlog}
131
+
132
+ ${usage.browse}
133
+ ${description.browse}
134
+
135
+ ${usage.play}
136
+ ${description.play}
137
+
138
+ ${usage.queue}
139
+ ${description.queue}
140
+
141
+ ${usage.pause}
142
+ ${description.pause}
143
+
144
+ ${usage.stop}
145
+ ${description.stop}
146
+
147
+ ${usage.next}
148
+ ${description.next}
149
+
150
+ ${usage.previous}
151
+ ${description.previous}
152
+
153
+ ${usage.sleepTimer}
154
+ ${description.sleepTimer}
155
+
156
+ ${usage.groupVolume}
157
+ ${description.groupVolume}
158
+
159
+ ${usage.groupMute}
160
+ ${description.groupMute}
161
+
162
+ ${usage.join}
163
+ ${description.join}
164
+
165
+ ${usage.leave}
166
+ ${description.leave}
167
+
168
+ ${usage.volume}
169
+ ${description.volume}
170
+
171
+ ${usage.mute}
172
+ ${description.mute}
173
+
174
+ ${usage.bass}
175
+ ${description.bass}
176
+
177
+ ${usage.treble}
178
+ ${description.treble}
179
+
180
+ ${usage.loudness}
181
+ ${description.loudness}
182
+
183
+ ${usage.balance}
184
+ ${description.balance}
185
+
186
+ ${usage.nightSound}
187
+ ${description.nightSound}
188
+
189
+ ${usage.speechEnhancement}
190
+ ${description.speechEnhancement}
191
+
192
+ ${usage.surroundEnable}
193
+ ${description.surroundEnable}
194
+
195
+ ${usage.tvLevel}
196
+ ${description.tvLevel}
197
+
198
+ ${usage.musicLevel}
199
+ ${description.musicLevel}
200
+
201
+ ${usage.musicPlaybackFull}
202
+ ${description.musicPlaybackFull}
203
+
204
+ ${usage.heightLevel}
205
+ ${description.heightLevel}
206
+
207
+ ${usage.subEnable}
208
+ ${description.subEnable}
209
+
210
+ ${usage.subLevel}
211
+ ${description.subLevel}
212
+
213
+ ${usage.led}
214
+ ${description.led}
215
+
216
+ ${usage.buttonLock}
217
+ ${description.buttonLock}
218
+
219
+ For more help, issue: ${b('zp')} ${u('command')} ${b('-h')}`,
220
+ info: `${description.info}
221
+
222
+ Usage: ${b('zp')} ${usage.info}
223
+
224
+ Parameters:
225
+ ${b('-h')}, ${b('--help')}
226
+ Print this help and exit.
227
+
228
+ ${b('-n')}, ${b('--noWhiteSpace')}
229
+ Do not include spaces nor newlines in JSON output.
230
+
231
+ ${b('-v')}. ${b('--verbose')}
232
+ Verbose. Include topology info.`,
233
+ description: `${description.description}
234
+
235
+ Usage: ${b('zp')} ${usage.description}
236
+
237
+ Parameters:
238
+ ${b('-h')}, ${b('--help')}
239
+ Print this help and exit.
240
+
241
+ ${b('-n')}, ${b('--noWhiteSpace')}
242
+ Do not include spaces nor newlines in JSON output.
243
+
244
+ ${b('-S')}, ${b('--scpd')}
245
+ Include service control point definitions.
246
+
247
+ ${b('-s')}, ${b('--sortKeys')}
248
+ Sort JSON object key/value pairs alphabetically on key.`,
249
+ topology: `${description.topology}
250
+
251
+ Usage: ${b('zp')} ${usage.topology}
252
+
253
+ Parameters:
254
+ ${b('-h')}, ${b('--help')}
255
+ Print this help and exit.
256
+
257
+ ${b('-n')}, ${b('--noWhiteSpace')}
258
+ Do not include spaces nor newlines in JSON output.
259
+
260
+ ${b('-p')}, ${b('--playersOnly')}
261
+ Do not include zones in the output.
262
+
263
+ ${b('-r')}, ${b('--raw')}
264
+ Output the raw zone group state.
265
+
266
+ ${b('-v')}, ${b('--verify')}
267
+ Verify that each zone player can be reached.
268
+ Include the device description information for reachable zone players.`,
269
+ eventlog: `${description.eventlog}
270
+
271
+ Usage: ${b('zp')} ${usage.eventlog}
272
+
273
+ Parameters:
274
+ ${b('-h')}, ${b('--help')}
275
+ Print this help and exit.
276
+
277
+ ${b('-n')}, ${b('--noWhiteSpace')}
278
+ Do not include spaces nor newlines in JSON output.
279
+
280
+ ${b('-s')}, ${b('--service')}
281
+ Do not output timestamps (useful when running as service).
282
+
283
+ ${b('-t')}, ${b('--topology')}
284
+ Show only parsed ZoneGroupTopology events, to monitor topology changes.`,
285
+ browse: `${description.browse}
286
+ Returns a list of media items with ${u('object')} for browsing or ${u('uri')} for playing.
287
+
288
+ Usage: ${b('zp')} ${usage.browse}
289
+
290
+ Parameters:
291
+ ${b('-h')}, ${b('--help')}
292
+ Print this help and exit.
293
+
294
+ ${b('-n')}, ${b('--noWhiteSpace')}
295
+ Do not include spaces nor newlines in JSON output.
296
+
297
+ ${u('object')}
298
+ Browse ${u('object')} instead of default (top level).
299
+ Use ${b('zp browse')} to obtain the value for ${u('object')}.`,
300
+ play: `${description.play}
301
+
302
+ Usage: ${b('zp')} ${usage.play}
303
+
304
+ Parameters:
305
+ ${b('-h')}, ${b('--help')}
306
+ Print this help and exit.
307
+
308
+ ${u('uri')}
309
+ Set source to ${u('uri')}.
310
+ Use ${b('zp browse')} to obtain the value for ${u('uri')}.
311
+
312
+ ${u('meta')}
313
+ Set meta data for source to ${u('meta')}.
314
+ Use ${b('zp browse')} to obtain the value for ${u('meta')}.`,
315
+ queue: `${description.queue}
316
+
317
+ Usage: ${b('zp')} ${usage.queue}
318
+
319
+ Parameters:
320
+ ${b('-h')}, ${b('--help')}
321
+ Print this help and exit.
322
+
323
+ ${u('uri')}
324
+ Set source to ${u('uri')}.
325
+ Use ${b('zp browse')} to obtain the value for ${u('uri')}.
326
+
327
+ ${u('meta')}
328
+ Set meta data for source to ${u('meta')}.
329
+ Use ${b('zp browse')} to obtain the value for ${u('meta')}.`,
330
+ pause: `${description.pause}
331
+
332
+ Usage: ${b('zp')} ${usage.pause}
333
+
334
+ Parameters:
335
+ ${b('-h')}, ${b('--help')}
336
+ Print this help and exit.`,
337
+ stop: `${description.stop}
338
+
339
+ Usage: ${b('zp')} ${usage.stop}
340
+
341
+ Parameters:
342
+ ${b('-h')}, ${b('--help')}
343
+ Print this help and exit.`,
344
+ next: `${description.next}
345
+
346
+ Usage: ${b('zp')} ${usage.stop}
347
+
348
+ Parameters:
349
+ ${b('-h')}, ${b('--help')}
350
+ Print this help and exit.`,
351
+ previous: `${description.previous}
352
+
353
+ Usage: ${b('zp')} ${usage.previous}
354
+
355
+ Parameters:
356
+ ${b('-h')}, ${b('--help')}
357
+ Print this help and exit.`,
358
+ crossfade: `${description.crossfade}
359
+
360
+ Usage: ${b('zp')} ${usage.crossfade}
361
+
362
+ Parameters:
363
+ ${b('-h')}, ${b('--help')}
364
+ Print this help and exit.
365
+
366
+ ${b('on')}
367
+ Set crossfade.
368
+
369
+ ${b('off')}
370
+ Clear crossfade.`,
371
+ repeat: `${description.repeat}
372
+
373
+ Usage: ${b('zp')} ${usage.repeat}
374
+
375
+ Parameters:
376
+ ${b('-h')}, ${b('--help')}
377
+ Print this help and exit.
378
+
379
+ ${b('on')}
380
+ Repeat all tracks.
381
+
382
+ ${b('1')}
383
+ Repeat current track.
384
+
385
+ ${b('off')}
386
+ Clear repeat.`,
387
+ shuffle: `${description.shuffle}
388
+
389
+ Usage: ${b('zp')} ${usage.shuffle}
390
+
391
+ Parameters:
392
+ ${b('-h')}, ${b('--help')}
393
+ Print this help and exit.
394
+
395
+ ${b('on')}
396
+ Set shuffle.
397
+
398
+ ${b('off')}
399
+ Clear shuffle.`,
400
+ sleepTimer: `${description.sleepTimer}
401
+
402
+ Usage: ${b('zp')} ${usage.sleepTimer}
403
+
404
+ Parameters:
405
+ ${b('-h')}, ${b('--help')}
406
+ Print this help and exit.
407
+
408
+ ${u('time')}
409
+ Set sleep timer to ${u('time')} (from ${b('00:00:00')} to ${b('23:59:59')}).
410
+
411
+ ${b('off')}
412
+ Clear sleep timer.`,
413
+ groupVolume: `${description.groupVolume}
414
+
415
+ Usage: ${b('zp')} ${usage.groupVolume}
416
+
417
+ Parameters:
418
+ ${b('-h')}, ${b('--help')}
419
+ Print this help and exit.
420
+
421
+ ${u('volume')}
422
+ Set group volume to ${u('volume')} (from 0 to 100).`,
423
+ groupMute: `${description.groupMute}
424
+
425
+ Usage: ${b('zp')} ${usage.groupMute}
426
+
427
+ Parameters:
428
+ ${b('-h')}, ${b('--help')}
429
+ Print this help and exit.
430
+
431
+ ${b('on')}
432
+ Set group mute.
433
+
434
+ ${b('off')}
435
+ Clear group mute.`,
436
+ join: `${description.join}
437
+
438
+ Usage: ${b('zp')} ${usage.join}
439
+
440
+ Parameters:
441
+ ${b('-h')}, ${b('--help')}
442
+ Print this help and exit.
443
+
444
+ ${u('zone')}
445
+ Join ${u('zone')}'s group.`,
446
+ leave: `${description.leave}
447
+
448
+ Usage: ${b('zp')} ${usage.leave}
449
+
450
+ Parameters:
451
+ ${b('-h')}, ${b('--help')}
452
+ Print this help and exit.`,
453
+ volume: `${description.volume}
454
+
455
+ Usage: ${b('zp')} ${usage.volume}
456
+
457
+ Parameters:
458
+ ${b('-h')}, ${b('--help')}
459
+ Print this help and exit.
460
+
461
+ ${u('volume')}
462
+ Set volume to ${u('volume')} (from 0 to 100).`,
463
+ mute: `${description.mute}
464
+
465
+ Usage: ${b('zp')} ${usage.mute}
466
+
467
+ Parameters:
468
+ ${b('-h')}, ${b('--help')}
469
+ Print this help and exit.
470
+
471
+ ${b('on')}
472
+ Set mute.
473
+
474
+ ${b('off')}
475
+ Clear mute.`,
476
+ bass: `${description.bass}
477
+
478
+ Usage: ${b('zp')} ${usage.bass}
479
+
480
+ Parameters:
481
+ ${b('-h')}, ${b('--help')}
482
+ Print this help and exit.
483
+
484
+ ${u('bass')}
485
+ Set bass to ${u('bass')} (from -10 to 10).`,
486
+ treble: `${description.treble}
487
+
488
+ Usage: ${b('zp')} ${usage.treble}
489
+
490
+ Parameters:
491
+ ${b('-h')}, ${b('--help')}
492
+ Print this help and exit.
493
+
494
+ ${u('treble')}
495
+ Set treble to ${u('treble')} (from -10 to 10).`,
496
+ loudness: `${description.loudness}
497
+
498
+ Usage: ${b('zp')} ${usage.loudness}
499
+
500
+ Parameters:
501
+ ${b('-h')}, ${b('--help')}
502
+ Print this help and exit.
503
+
504
+ ${b('on')}
505
+ Set loudness.
506
+
507
+ ${b('off')}
508
+ Clear loudness.`,
509
+ balance: `${description.balance}
510
+
511
+ Usage: ${b('zp')} ${usage.balance}
512
+
513
+ Parameters:
514
+ ${b('-h')}, ${b('--help')}
515
+ Print this help and exit.
516
+
517
+ ${u('balance')}
518
+ Set balance to ${u('balance')} (from -100 to 100).`,
519
+ nightSound: `${description.nightSound}
520
+
521
+ Usage: ${b('zp')} ${usage.nightSound}
522
+
523
+ Parameters:
524
+ ${b('-h')}, ${b('--help')}
525
+ Print this help and exit.
526
+
527
+ ${b('on')}
528
+ Set night sound.
529
+
530
+ ${b('off')}
531
+ Clear night sound.`,
532
+ speechEnhancement: `${description.speechEnhancement}
533
+
534
+ Usage: ${b('zp')} ${usage.speechEnhancement}
535
+
536
+ Parameters:
537
+ ${b('-h')}, ${b('--help')}
538
+ Print this help and exit.
539
+
540
+ ${b('on')}
541
+ Set speech enhancement.
542
+
543
+ ${b('off')}
544
+ Clear speech enhancement.`,
545
+ surroundEnable: `${description.surroundEnable}
546
+
547
+ Usage: ${b('zp')} ${usage.surroundEnable}
548
+
549
+ Parameters:
550
+ ${b('-h')}, ${b('--help')}
551
+ Print this help and exit.
552
+
553
+ ${b('on')}
554
+ Enable surround speakers.
555
+
556
+ ${b('off')}
557
+ Disable surround speakers.`,
558
+ tvLevel: `${description.tvLevel}
559
+
560
+ Usage: ${b('zp')} ${usage.tvLevel}
561
+
562
+ Parameters:
563
+ ${b('-h')}, ${b('--help')}
564
+ Print this help and exit.
565
+
566
+ ${u('level')}
567
+ Set TV surround level to ${u('level')} (from -15 to 15).`,
568
+ musicLevel: `${description.musicLevel}
569
+
570
+ Usage: ${b('zp')} ${usage.musicLevel}
571
+
572
+ Parameters:
573
+ ${b('-h')}, ${b('--help')}
574
+ Print this help and exit.
575
+
576
+ ${u('level')}
577
+ Set music surround level to ${u('level')} (from -15 to 15).`,
578
+ musicPlaybackFull: `${description.musicPlaybackFull}
579
+
580
+ Usage: ${b('zp')} ${usage.musicPlaybackFull}
581
+
582
+ Parameters:
583
+ ${b('-h')}, ${b('--help')}
584
+ Print this help and exit.
585
+
586
+ ${b('on')}
587
+ Set music playback to full.
588
+
589
+ ${b('off')}
590
+ Set music playback to ambient.`,
591
+ heightLevel: `${description.heightLevel}
592
+
593
+ Usage: ${b('zp')} ${usage.heightLevel}
594
+
595
+ Parameters:
596
+ ${b('-h')}, ${b('--help')}
597
+ Print this help and exit.
598
+
599
+ ${u('level')}
600
+ Set height channel level to ${u('level')} (from -10 to 10).`,
601
+ subEnable: `${description.subEnable}
602
+
603
+ Usage: ${b('zp')} ${usage.subEnable}
604
+
605
+ Parameters:
606
+ ${b('-h')}, ${b('--help')}
607
+ Print this help and exit.
608
+
609
+ ${b('on')}
610
+ Enable Sub.
611
+
612
+ ${b('off')}
613
+ Disable Sub.`,
614
+ subLevel: `${description.subLevel}
615
+
616
+ Usage: ${b('zp')} ${usage.subLevel}
617
+
618
+ Parameters:
619
+ ${b('-h')}, ${b('--help')}
620
+ Print this help and exit.
621
+
622
+ ${u('level')}
623
+ Set Sub level to ${u('level')} (from -15 to 15).`,
624
+ led: `${description.led}
625
+
626
+ Usage: ${b('zp')} ${usage.led}
627
+
628
+ Parameters:
629
+ ${b('-h')}, ${b('--help')}
630
+ Print this help and exit.
631
+
632
+ ${b('on')}
633
+ Set ZonePlayer LED on.
634
+
635
+ ${b('off')}
636
+ Set ZonePlayer LED off.`,
637
+ buttonLock: `${description.buttonLock}
638
+
639
+ Usage: ${b('zp')} ${usage.buttonLock}
640
+
641
+ Parameters:
642
+ ${b('-h')}, ${b('--help')}
643
+ Print this help and exit.
644
+
645
+ ${b('on')}
646
+ Set ZonePlayer button lock state (i.e. disable ZonePlayer buttons).
647
+
648
+ ${b('off')}
649
+ Clear ZonePlayer button lock state (i.e. enable ZonePlayer buttons).`
650
+ }
651
+
652
+ const unsupportedServices = [
653
+ 'ConnectionManager', // No useful information.
654
+ 'MusicServices', // Not supported by homebridge-zp.
655
+ 'QPlay', // Doesn't support SUBSCRIBE.
656
+ 'Queue' // Not supported by homebridge-zp.
657
+ ]
658
+
659
+ class ZpTool extends CommandLineTool {
660
+ constructor (pkgJson) {
661
+ super()
662
+ this.pkgJson = pkgJson
663
+ this.usage = usage.zp
664
+ this.clients = []
665
+ }
666
+
667
+ createZpClient (options) {
668
+ const zpClient = new ZpClient(options)
669
+ zpClient
670
+ .on('error', (error) => {
671
+ if (error.request == null) {
672
+ this.warn(error)
673
+ return
674
+ }
675
+ if (error.request.id !== this.requestId) {
676
+ if (error.request.body == null) {
677
+ this.log(
678
+ '%s: request %d: %s %s', error.request.name, error.request.id,
679
+ error.request.method, error.request.resource
680
+ )
681
+ } else {
682
+ this.log(
683
+ '%s: request %d: %s %s', error.request.name, error.request.id,
684
+ error.request.method, error.request.resource, error.request.action
685
+ )
686
+ }
687
+ this.requestId = error.request.id
688
+ }
689
+ this.warn(
690
+ '%s: request %d: %s', error.request.name, error.request.id, error
691
+ )
692
+ })
693
+ // TODO: never called, since no UPnP listener is active
694
+ .on('rebooted', (oldBootSeq) => {
695
+ this.debug(
696
+ '%s: rebooted (%d -> %d)', zpClient.name,
697
+ oldBootSeq, zpClient.bootSeq
698
+ )
699
+ })
700
+ .on('addressChanged', (oldAddress) => {
701
+ this.debug(
702
+ '%s: IP address changed (%s -> %s)', zpClient.name,
703
+ oldAddress, zpClient.address
704
+ )
705
+ })
706
+ .on('request', (request) => {
707
+ this.debug(
708
+ '%s: request %s: %s %s%s', request.name,
709
+ request.id, request.method, request.resource,
710
+ request.action == null ? '' : ' ' + request.action
711
+ )
712
+ if (request.parsedBody != null) {
713
+ this.vdebug(
714
+ '%s: request %s: %s %s %j', request.name,
715
+ request.id, request.method, request.url, request.parsedBody
716
+ )
717
+ this.vvdebug(
718
+ '%s: request %s: %s %s (headers: %j) %j', request.name,
719
+ request.id, request.method, request.url,
720
+ request.headers, request.body
721
+ )
722
+ } else {
723
+ this.vdebug(
724
+ '%s: request %s: %s %s', request.name,
725
+ request.id, request.method, request.url
726
+ )
727
+ this.vvdebug(
728
+ '%s: request %s: %s %s (headers: %j)', request.name,
729
+ request.id, request.method, request.url, request.headers
730
+ )
731
+ }
732
+ })
733
+ .on('response', (response) => {
734
+ this.debug(
735
+ '%s: request %d: %d %s', response.request.name,
736
+ response.request.id, response.statusCode, response.statusMessage
737
+ )
738
+ if (response.parsedBody != null) {
739
+ this.vvdebug(
740
+ '%s: request %d: response (headers: %j): %j', response.request.name,
741
+ response.request.id, response.headers, response.body
742
+ )
743
+ this.vdebug(
744
+ '%s: request %d: response: %j', response.request.name,
745
+ response.request.id, response.parsedBody
746
+ )
747
+ }
748
+ })
749
+ .on('message', (message) => {
750
+ const notify = message.device === 'ZonePlayer'
751
+ ? message.service
752
+ : message.device + '/' + message.service
753
+ this.vvdebug(
754
+ '%s: notify %s/Event: %s', this._clargs.options.host,
755
+ notify, message.body
756
+ )
757
+ this.vdebug(
758
+ '%s: notify %s/Event: %j', this._clargs.options.host,
759
+ notify, message.parsedBody
760
+ )
761
+ this.debug('%s: notify %s/Event', this._clargs.options.host, notify)
762
+ })
763
+ return zpClient
764
+ }
765
+
766
+ async main () {
767
+ try {
768
+ this._clargs = this.parseArguments()
769
+ if (this._clargs.options.host == null || this._clargs.options.host === '') {
770
+ throw new UsageError(`Missing host. Set ${b('ZP_HOST')} or specify ${b('-H')}.`)
771
+ }
772
+ this.zpListener = new ZpListener()
773
+ this.zpListener
774
+ .on('listening', (url) => { this.debug('listening on %s', url) })
775
+ .on('close', (url) => { this.debug('closed %s', url) })
776
+ .on('error', (error) => { this.warn(error) })
777
+ this._clargs.options.listener = this.zpListener
778
+ this.zpClient = await this.createZpClient(this._clargs.options)
779
+ await this.zpClient.init()
780
+ this.debug(
781
+ '%s: reached using local address %s', this._clargs.options.host,
782
+ this.zpClient.localAddress
783
+ )
784
+ this.name = 'zp ' + this._clargs.command
785
+ this.usage = `${b('zp')} ${usage[this._clargs.command]}`
786
+ this.help = help[this._clargs.command]
787
+ await this[this._clargs.command](this._clargs.args)
788
+ } catch (error) {
789
+ this.error(error)
790
+ }
791
+ }
792
+
793
+ parseArguments () {
794
+ const parser = new CommandLineParser(this.pkgJson)
795
+ const clargs = {
796
+ options: {
797
+ host: process.env.ZP_HOST,
798
+ timeout: 5
799
+ }
800
+ }
801
+ parser
802
+ .help('h', 'help', help.zp)
803
+ .version('V', 'version')
804
+ .flag('D', 'debug', () => {
805
+ if (this.vdebugEnabled) {
806
+ this.setOptions({ vvdebug: true })
807
+ } else if (this.debugEnabled) {
808
+ this.setOptions({ vdebug: true })
809
+ } else {
810
+ this.setOptions({ debug: true, chalk: true })
811
+ }
812
+ })
813
+ .option('H', 'host', (value) => {
814
+ OptionParser.toHost('host', value, false, true)
815
+ clargs.options.host = value
816
+ })
817
+ .option('t', 'timeout', (value) => {
818
+ clargs.options.timeout = OptionParser.toInt(
819
+ 'timeout', value, 1, 60, true
820
+ )
821
+ })
822
+ .parameter('command', (value) => {
823
+ if (usage[value] == null || typeof this[value] !== 'function') {
824
+ throw new UsageError(`${value}: unknown command`)
825
+ }
826
+ clargs.command = value
827
+ })
828
+ .remaining((list) => { clargs.args = list })
829
+ .parse()
830
+ return clargs
831
+ }
832
+
833
+ async destroy () {
834
+ await this.zpClient.close()
835
+ }
836
+
837
+ async info (...args) {
838
+ const parser = new CommandLineParser(this.pkgJson)
839
+ const clargs = {
840
+ options: { sortKeys: true }
841
+ }
842
+ parser.help('h', 'help', this.help)
843
+ parser.flag('n', 'noWhiteSpace', () => {
844
+ clargs.options.noWhiteSpace = true
845
+ })
846
+ parser.flag('v', 'verbose', () => { clargs.verbose = true })
847
+ parser.parse(...args)
848
+ const jsonFormatter = new JsonFormatter(clargs.options)
849
+ if (clargs.verbose) {
850
+ await this.zpClient.initTopology()
851
+ }
852
+ this.print(jsonFormatter.stringify(this.zpClient.info))
853
+ }
854
+
855
+ async description (...args) {
856
+ const parser = new CommandLineParser(this.pkgJson)
857
+ const clargs = {
858
+ options: {}
859
+ }
860
+ parser.help('h', 'help', this.help)
861
+ parser.flag('n', 'noWhiteSpace', () => {
862
+ clargs.options.noWhiteSpace = true
863
+ })
864
+ parser.flag('S', 'scpd', () => { clargs.scpd = true })
865
+ parser.flag('s', 'sortKeys', () => { clargs.options.sortKeys = true })
866
+ parser.parse(...args)
867
+ const jsonFormatter = new JsonFormatter(clargs.options)
868
+ const response = this.zpClient.description
869
+ if (clargs.scpd) {
870
+ const devices = [response.device]
871
+ .concat(response.device.deviceList)
872
+ for (const device of devices) {
873
+ for (const service of device.serviceList) {
874
+ service.scpd = await this.zpClient.get(service.scpdUrl)
875
+ }
876
+ }
877
+ }
878
+ const json = jsonFormatter.stringify(response)
879
+ this.print(json)
880
+ }
881
+
882
+ async _getInfo (zonePlayer) {
883
+ const zpClient = this.createZpClient({
884
+ host: zonePlayer.address,
885
+ id: zonePlayer.id,
886
+ timeout: this._clargs.options.timeout
887
+ })
888
+ await zpClient.init()
889
+ await zpClient.initTopology(this.zpClient)
890
+ return zpClient.info
891
+ }
892
+
893
+ async topology (...args) {
894
+ const parser = new CommandLineParser(this.pkgJson)
895
+ const clargs = {
896
+ options: {}
897
+ }
898
+ parser.help('h', 'help', this.help)
899
+ parser.flag('n', 'noWhiteSpace', () => {
900
+ clargs.options.noWhiteSpace = true
901
+ })
902
+ parser.flag('p', 'playersOnly', () => { clargs.players = true })
903
+ parser.flag('r', 'raw', () => { clargs.raw = true })
904
+ parser.flag('v', 'verify', () => { clargs.verify = true })
905
+ parser.parse(...args)
906
+ const jsonFormatter = new JsonFormatter(clargs.options)
907
+ await this.zpClient.initTopology()
908
+ let result = {}
909
+ if (clargs.verify) {
910
+ const zonePlayers = this.zpClient.zonePlayers
911
+ const jobs = []
912
+ for (const id in zonePlayers) {
913
+ if (id === this.zpClient.id) {
914
+ result[id] = this.zpClient.info
915
+ } else {
916
+ result[id] = undefined
917
+ const zonePlayer = this.zpClient.zonePlayers[id]
918
+ if (zonePlayer == null) {
919
+ delete result[id]
920
+ this.error('%s: zone player not found', id)
921
+ continue
922
+ }
923
+ jobs.push(this._getInfo(zonePlayer)
924
+ .then((info) => {
925
+ result[id] = info
926
+ }).catch(() => {
927
+ // delete result[id]
928
+ })
929
+ )
930
+ }
931
+ }
932
+ for (const job of jobs) {
933
+ await job
934
+ }
935
+ if (!clargs.players) {
936
+ result = ZpClient.unflatten(result)
937
+ }
938
+ } else if (clargs.players) {
939
+ result = this.zpClient.zonePlayers
940
+ } else if (clargs.raw) {
941
+ result = this.zpClient.zoneGroupState
942
+ } else {
943
+ result = this.zpClient.zones
944
+ }
945
+ const json = jsonFormatter.stringify(result)
946
+ this.print(json)
947
+ }
948
+
949
+ async eventlog (...args) {
950
+ const parser = new CommandLineParser(this.pkgJson)
951
+ const clargs = {
952
+ mode: 'daemon',
953
+ options: {}
954
+ }
955
+ parser.help('h', 'help', this.help)
956
+ parser.flag('n', 'noWhiteSpace', () => {
957
+ clargs.options.noWhiteSpace = true
958
+ })
959
+ parser.flag('s', 'service', () => { clargs.mode = 'service' })
960
+ parser.flag('t', 'topology', () => { clargs.topology = true })
961
+ parser.parse(...args)
962
+ this.setOptions({ mode: clargs.mode })
963
+ const jsonFormatter = new JsonFormatter(clargs.options)
964
+ this.zpClient
965
+ .on('message', (message) => {
966
+ if (clargs.topology) {
967
+ if (message.service === 'ZoneGroupTopology') {
968
+ this.log(
969
+ '%s: topology %s', this._clargs.options.host,
970
+ jsonFormatter.stringify(this.zpClient.zones)
971
+ )
972
+ }
973
+ } else {
974
+ this.log(
975
+ '%s: %s %s event: %s', this._clargs.options.host,
976
+ message.device, message.service,
977
+ jsonFormatter.stringify(message.parsedBody)
978
+ )
979
+ }
980
+ })
981
+ await this.zpClient.open()
982
+ if (clargs.topology) {
983
+ try {
984
+ await this.zpClient.subscribe('/ZoneGroupTopology/Event')
985
+ } catch (error) {
986
+ this.error(error)
987
+ }
988
+ } else {
989
+ const description = this.zpClient.description
990
+ const deviceList = [description.device].concat(description.device.deviceList)
991
+ for (const device of deviceList) {
992
+ for (const service of device.serviceList) {
993
+ const serviceName = service.serviceId.split(':')[3]
994
+ if (unsupportedServices.includes(serviceName)) {
995
+ continue
996
+ }
997
+ try {
998
+ await this.zpClient.subscribe(service.eventSubUrl)
999
+ } catch (error) {
1000
+ this.error(error)
1001
+ }
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ async browse (...args) {
1008
+ const clargs = { options: {} }
1009
+ const parser = new CommandLineParser(this.pkgJson)
1010
+ parser.help('h', 'help', this.help)
1011
+ parser.flag('n', 'noWhiteSpace', () => {
1012
+ clargs.options.noWhiteSpace = true
1013
+ })
1014
+ parser.remaining((list) => {
1015
+ if (list.length > 1) {
1016
+ throw new UsageError('too many arguments')
1017
+ }
1018
+ clargs.object = list[0]
1019
+ })
1020
+ parser.parse(...args)
1021
+ const jsonFormatter = new JsonFormatter(clargs.options)
1022
+ await this.zpClient.initTopology()
1023
+ let result
1024
+ if (clargs.object == null) {
1025
+ result = {
1026
+ 'Music Library': { browse: 'A:' },
1027
+ 'Music Library Servers': { browse: 'S:' },
1028
+ 'Sonos Favorites': { browse: 'FV:' },
1029
+ 'Sonos Playlists': { browse: 'SQ:' },
1030
+ 'Sonos Queues': { browse: 'Q:' }
1031
+ }
1032
+ if (this.zpClient.airPlay) {
1033
+ result.AirPlay = {
1034
+ uri: 'x-sonosapi-vli:' + this.zpClient.id
1035
+ }
1036
+ }
1037
+ if (this.zpClient.audioIn) {
1038
+ result['Audio In'] = {
1039
+ uri: 'x-rincon-stream:' + this.zpClient.id
1040
+ }
1041
+ }
1042
+ if (this.zpClient.tvIn) {
1043
+ result.TV = {
1044
+ uri: 'x-sonos-htastream:' + this.zpClient.id + ':spdif'
1045
+ }
1046
+ }
1047
+ } else {
1048
+ result = await this.zpClient.browse(clargs.object)
1049
+ }
1050
+ const json = jsonFormatter.stringify(result)
1051
+ this.print(json)
1052
+ }
1053
+
1054
+ async play (...args) {
1055
+ let uri
1056
+ let meta
1057
+ const parser = new CommandLineParser(this.pkgJson)
1058
+ parser.help('h', 'help', this.help)
1059
+ parser.remaining((list) => {
1060
+ if (list.length > 2) {
1061
+ throw new UsageError('too many arguments')
1062
+ }
1063
+ uri = list[0]
1064
+ meta = list[1]
1065
+ })
1066
+ parser.parse(...args)
1067
+ if (uri != null) {
1068
+ await this.zpClient.setAvTransportUri(uri, meta)
1069
+ }
1070
+ await this.zpClient.play()
1071
+ }
1072
+
1073
+ async queue (...args) {
1074
+ let uri
1075
+ let meta
1076
+ const parser = new CommandLineParser(this.pkgJson)
1077
+ parser.help('h', 'help', this.help)
1078
+ parser.parameter('uri', (value) => {
1079
+ uri = value
1080
+ })
1081
+ parser.remaining((list) => {
1082
+ if (list.length > 1) {
1083
+ throw new UsageError('too many arguments')
1084
+ }
1085
+ meta = list[0]
1086
+ })
1087
+ parser.parse(...args)
1088
+ await this.zpClient.setAvTransportQueue(uri, meta)
1089
+ }
1090
+
1091
+ async pause (...args) { return this.simpleCommand('pause', ...args) }
1092
+
1093
+ async stop (...args) { return this.simpleCommand('stop', ...args) }
1094
+
1095
+ async next (...args) { return this.simpleCommand('next', ...args) }
1096
+
1097
+ async previous (...args) { return this.simpleCommand('previous', ...args) }
1098
+
1099
+ async crossfade (...args) { return this.onOffCommand('CrossfadeMode', ...args) }
1100
+
1101
+ async repeat (...args) {
1102
+ const parser = new CommandLineParser(this.pkgJson)
1103
+ let repeat
1104
+ parser.help('h', 'help', this.help)
1105
+ parser.remaining((list) => {
1106
+ if (list.length > 1) {
1107
+ throw new UsageError('too many arguments')
1108
+ }
1109
+ if (list.length === 1) {
1110
+ if (['on', '1', 'off'].includes(list[0])) {
1111
+ repeat = list[0]
1112
+ } else {
1113
+ throw new UsageError(`${list[0]}: invalid repeat value`)
1114
+ }
1115
+ }
1116
+ })
1117
+ parser.parse(...args)
1118
+ if (repeat != null) {
1119
+ await this.zpClient.setRepeat(repeat)
1120
+ }
1121
+ this.print(await this.zpClient.getRepeat())
1122
+ }
1123
+
1124
+ async shuffle (...args) { return this.onOffCommand('Shuffle', ...args) }
1125
+
1126
+ async sleepTimer (...args) {
1127
+ const parser = new CommandLineParser(this.pkgJson)
1128
+ let duration
1129
+ parser.help('h', 'help', this.help)
1130
+ parser.remaining((list) => {
1131
+ if (list.length > 1) {
1132
+ throw new UsageError('too many arguments')
1133
+ }
1134
+ if (list.length === 1) {
1135
+ if (/^(:?2[0-3]|[01][0-9]):[0-5][0-9]:[0-5][0-9]$/.test(list[0])) {
1136
+ duration = list[0]
1137
+ } else if (list[0] === 'off') {
1138
+ duration = ''
1139
+ } else {
1140
+ throw new UsageError(`${list[0]}: invalid duration`)
1141
+ }
1142
+ }
1143
+ })
1144
+ parser.parse(...args)
1145
+ if (duration != null) {
1146
+ await this.zpClient.setSleepTimer(duration)
1147
+ }
1148
+ const timer = await this.zpClient.getSleepTimer()
1149
+ this.print(timer === '' ? 'off' : timer)
1150
+ }
1151
+
1152
+ async groupVolume (...args) { return this.valueCommand('GroupVolume', 0, 100, ...args) }
1153
+
1154
+ async groupMute (...args) { return this.onOffCommand('GroupMute', ...args) }
1155
+
1156
+ async join (...args) {
1157
+ let coordinator
1158
+ const parser = new CommandLineParser(this.pkgJson)
1159
+ parser.help('h', 'help', this.help)
1160
+ parser.parameter('zone', (value) => {
1161
+ coordinator = value
1162
+ })
1163
+ parser.parse(...args)
1164
+ await this.zpClient.initTopology()
1165
+ for (const id in this.zpClient.zones) {
1166
+ if (this.zpClient.zones[id].zoneName === coordinator) {
1167
+ return this.zpClient.setAvTransportUri('x-rincon:' + id)
1168
+ }
1169
+ }
1170
+ throw new Error(`${coordinator}: zone not found`)
1171
+ }
1172
+
1173
+ async leave (...args) { return this.simpleCommand('becomeCoordinatorOfStandaloneGroup', ...args) }
1174
+
1175
+ async volume (...args) { return this.valueCommand('Volume', 0, 100, ...args) }
1176
+
1177
+ async mute (...args) { return this.onOffCommand('Mute', ...args) }
1178
+
1179
+ async bass (...args) { return this.valueCommand('Bass', -10, 10, ...args) }
1180
+
1181
+ async treble (...args) { return this.valueCommand('Treble', -10, 10, ...args) }
1182
+
1183
+ async balance (...args) { return this.valueCommand('Balance', -100, 100, ...args) }
1184
+
1185
+ async loudness (...args) { return this.onOffCommand('Loudness', ...args) }
1186
+
1187
+ async nightSound (...args) { return this.onOffCommand('NightSound', ...args) }
1188
+
1189
+ async speechEnhancement (...args) { return this.onOffCommand('SpeechEnhancement', ...args) }
1190
+
1191
+ async surroundEnable (...args) { return this.onOffCommand('SurroundEnable', ...args) }
1192
+
1193
+ async tvLevel (...args) { return this.valueCommand('TvLevel', -15, 15, ...args) }
1194
+
1195
+ async musicLevel (...args) { return this.valueCommand('MusicLevel', -15, 15, ...args) }
1196
+
1197
+ async musicPlaybackFull (...args) { return this.onOffCommand('MusicPlaybackFull', ...args) }
1198
+
1199
+ async heightLevel (...args) { return this.valueCommand('HeightLevel', -10, 10, ...args) }
1200
+
1201
+ async subEnable (...args) { return this.onOffCommand('SubEnable', ...args) }
1202
+
1203
+ async subLevel (...args) { return this.valueCommand('SubLevel', -15, 15, ...args) }
1204
+
1205
+ async led (...args) { return this.onOffCommand('LedState', ...args) }
1206
+
1207
+ async buttonLock (...args) { return this.onOffCommand('ButtonLockState', ...args) }
1208
+
1209
+ async simpleCommand (command, ...args) {
1210
+ const parser = new CommandLineParser(this.pkgJson)
1211
+ parser.help('h', 'help', this.help)
1212
+ parser.parse(...args)
1213
+ return this.zpClient[command]()
1214
+ }
1215
+
1216
+ async valueCommand (command, min, max, ...args) {
1217
+ const parser = new CommandLineParser(this.pkgJson)
1218
+ let value
1219
+ parser.help('h', 'help', this.help)
1220
+ parser.remaining((list) => {
1221
+ if (list.length > 1) {
1222
+ throw new UsageError('too many arguments')
1223
+ }
1224
+ if (list.length === 1) {
1225
+ value = OptionParser.toInt(
1226
+ command.toLowerCase(), list[0], min, max, true
1227
+ )
1228
+ }
1229
+ })
1230
+ parser.parse(...args)
1231
+ if (value != null) {
1232
+ await this.zpClient['set' + command](value)
1233
+ }
1234
+ this.print('' + await this.zpClient['get' + command]())
1235
+ }
1236
+
1237
+ async onOffCommand (command, ...args) {
1238
+ const parser = new CommandLineParser(this.pkgJson)
1239
+ let value
1240
+ if (args.length > 1) {
1241
+ throw new UsageError('too many arguments')
1242
+ }
1243
+ parser.help('h', 'help', this.help)
1244
+ parser.remaining((list) => {
1245
+ if (list.length > 1) {
1246
+ throw new UsageError('too many arguments')
1247
+ }
1248
+ if (list.length === 1) {
1249
+ value = OptionParser.toBool(
1250
+ command.toLowerCase(), list[0], true
1251
+ )
1252
+ }
1253
+ })
1254
+ parser.parse(...args)
1255
+ if (value != null) {
1256
+ await this.zpClient['set' + command](value)
1257
+ }
1258
+ this.print((await this.zpClient['get' + command]()) ? 'on' : 'off')
1259
+ }
1260
+ }
1261
+
1262
+ export { ZpTool }