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/LICENSE +201 -0
- package/README.md +29 -0
- package/cli/zp.js +15 -0
- package/index.js +9 -0
- package/jsdoc.json +21 -0
- package/lib/ZpClient.js +1736 -0
- package/lib/ZpListener.js +176 -0
- package/lib/ZpTool.js +1262 -0
- package/lib/ZpXmlParser.js +291 -0
- package/package.json +50 -0
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 }
|