vsslctrl 0.1.9.dev1__tar.gz → 0.1.11.dev1__tar.gz
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.
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/PKG-INFO +127 -49
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/README.md +126 -48
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/pyproject.toml +1 -1
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/tests/test_integration.py +119 -98
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/__init__.py +3 -2
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/api_alpha.py +387 -233
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/core.py +14 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/data_structure.py +133 -31
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/decorators.py +54 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/device.py +75 -19
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/group.py +32 -7
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/io.py +95 -24
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/settings.py +49 -23
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl.egg-info/PKG-INFO +127 -49
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/LICENSE +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/setup.cfg +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/tests/test_event_bus.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/tests/test_settings.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/tests/test_zone.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/api_base.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/api_bravo.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/discovery.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/event_bus.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/exceptions.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/track.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/transport.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/utils.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl/zone.py +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl.egg-info/SOURCES.txt +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl.egg-info/dependency_links.txt +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl.egg-info/requires.txt +0 -0
- {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.11.dev1}/vsslctrl.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: vsslctrl
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11.dev1
|
|
4
4
|
Summary: Package for controlling VSSL's range of streaming amplifiers
|
|
5
5
|
Author-email: vsslctrl <vsslcontrolled@proton.me>
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -30,8 +30,13 @@ Requires-Dist: zeroconf==0.132.2; extra == "optional"
|
|
|
30
30
|
I am looking for testers with any VSSL amplifier models, please get in touch if you interested in helping. <vsslcontrolled@proton.me>
|
|
31
31
|
|
|
32
32
|
Tested on:
|
|
33
|
-
-
|
|
34
|
-
|
|
33
|
+
- **A.3x** software version **p15305.016.3701**
|
|
34
|
+
|
|
35
|
+
Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) with basic functionality working on:
|
|
36
|
+
- **A.1** software version **p15265.033.3703** (reported)
|
|
37
|
+
- **A.3** software version **p12013.141.3703** (reported)
|
|
38
|
+
- **A.3x** software version **p15305.016.3701**
|
|
39
|
+
- **A.6x** software version **p15305.017.3701** (reported)
|
|
35
40
|
|
|
36
41
|
## Important
|
|
37
42
|
|
|
@@ -39,52 +44,80 @@ There should not be any *[VSSL Agent's](https://vssl.gitbook.io/vssl-rest-api/ge
|
|
|
39
44
|
|
|
40
45
|
## TODOs
|
|
41
46
|
|
|
42
|
-
*
|
|
47
|
+
* Correct IO mapping between models and versions
|
|
43
48
|
* Better test coverage
|
|
49
|
+
* A.1(x) testing
|
|
44
50
|
|
|
45
51
|
## Basic Usage
|
|
46
52
|
|
|
47
53
|
`vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
|
|
48
54
|
|
|
55
|
+
### A.1 Example
|
|
56
|
+
|
|
49
57
|
```python
|
|
50
58
|
import asyncio
|
|
51
59
|
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
52
60
|
|
|
53
61
|
async def main():
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
|
|
63
|
+
# Represents a physical VSSL amplifier
|
|
64
|
+
vssl = Vssl(DeviceModels.A1)
|
|
65
|
+
a1 = vssl.add_zone(ZoneIDs.A1, '192.168.1.10')
|
|
66
|
+
|
|
67
|
+
# Connect and initiate zones.
|
|
68
|
+
await vssl.initialise()
|
|
69
|
+
|
|
70
|
+
"""Control Examples"""
|
|
71
|
+
# Print device name
|
|
72
|
+
print(a1.settings.name)
|
|
73
|
+
# Set volume to 25%
|
|
74
|
+
a1.volume = 25
|
|
75
|
+
# Pause
|
|
76
|
+
a1.pause()
|
|
77
|
+
# Print track name
|
|
78
|
+
print(a1.track.name)
|
|
79
|
+
|
|
80
|
+
# Shutdown and disconnect all zones
|
|
81
|
+
await vssl.shutdown()
|
|
57
82
|
|
|
58
|
-
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
59
85
|
|
|
60
|
-
|
|
86
|
+
### A.3x Example
|
|
61
87
|
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
91
|
+
|
|
92
|
+
async def main():
|
|
93
|
+
|
|
94
|
+
# Represents a physical VSSL amplifier
|
|
95
|
+
# If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
|
|
96
|
+
vssl = Vssl(DeviceModels.A3X)
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
98
|
+
# Add each you wish to control
|
|
99
|
+
zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
|
|
100
|
+
zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
|
|
101
|
+
zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
|
|
102
|
+
#... up to 6 zones
|
|
70
103
|
|
|
71
|
-
|
|
72
|
-
|
|
104
|
+
# Connect and initiate zones.
|
|
105
|
+
await vssl.initialise()
|
|
73
106
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
107
|
+
"""Control Examples"""
|
|
108
|
+
# Print zone1 name
|
|
109
|
+
print(zone1.settings.name)
|
|
110
|
+
# Set zone2 volume to 25%
|
|
111
|
+
zone2.volume = 25
|
|
112
|
+
# Pause zone3
|
|
113
|
+
zone3.pause()
|
|
114
|
+
# or zone3.transport.pause()
|
|
115
|
+
# Print zone1 track name
|
|
116
|
+
print(zone1.track.name)
|
|
84
117
|
|
|
85
118
|
|
|
86
|
-
|
|
87
|
-
|
|
119
|
+
# Shutdown and disconnect all zones
|
|
120
|
+
await vssl.shutdown()
|
|
88
121
|
|
|
89
122
|
|
|
90
123
|
asyncio.run(main())
|
|
@@ -107,7 +140,7 @@ print(zone_name)
|
|
|
107
140
|
>>> 'Living Room'
|
|
108
141
|
```
|
|
109
142
|
|
|
110
|
-
**Important** in the above example, `zone1.settings.name` wont be set to its new value until after the VSSL device has changed the name and the `Zone` class has received
|
|
143
|
+
**Important** in the above example, `zone1.settings.name` wont be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events.
|
|
111
144
|
|
|
112
145
|
|
|
113
146
|
# `Vssl`
|
|
@@ -118,26 +151,37 @@ print(zone_name)
|
|
|
118
151
|
| `serial` | Serial number | `str` readonly
|
|
119
152
|
| `model` | Device Model | `int` readonly
|
|
120
153
|
| `reboot()` | Reboot all zones | `func` |
|
|
154
|
+
| `factory_reset()` | Factory reset device | `func` |
|
|
155
|
+
|
|
121
156
|
|
|
122
157
|
```python
|
|
123
158
|
"""Example"""
|
|
124
159
|
# Reboot all zones
|
|
125
160
|
vssl.reboot()
|
|
161
|
+
# Do a factory reset (reset all settings)
|
|
162
|
+
vssl.factory_reset()
|
|
126
163
|
```
|
|
127
164
|
|
|
128
165
|
## `Vssl.settings`
|
|
129
166
|
|
|
130
|
-
| Property | Description | Type |
|
|
131
|
-
| ---------------------- | ----------- | ----------- |
|
|
132
|
-
| `name` | Device name | `str`
|
|
133
|
-
| `
|
|
167
|
+
| Property | Description | Type | Notes / Defaults |
|
|
168
|
+
| ---------------------- | ----------- | ----------- | ----------- |
|
|
169
|
+
| `name` | Device name | `str` |
|
|
170
|
+
| `bus_1_name` | Name of Bus 1 | `str` | <ul><li>A.1: Optical Input</li><li>A.3x/6x: Not Used</li></ul>
|
|
171
|
+
| `bus_2_name` | Name of Bus 2 | `str` | <ul><li>A.1: Coax Input</li><li>A.3x/6x: Optical Input</li></ul>
|
|
172
|
+
| `bluetooth` | Bluetooth enabled / disabled | `bool` |
|
|
173
|
+
| `bluetooth_toggle()` | Toggle Bluetooth | `func` |
|
|
134
174
|
|
|
135
175
|
```python
|
|
136
176
|
"""Example"""
|
|
137
177
|
# Setting device name
|
|
138
178
|
vssl.settings.name = 'My House'
|
|
139
|
-
# Setting
|
|
140
|
-
vssl.settings.
|
|
179
|
+
# Setting bus 2 name
|
|
180
|
+
vssl.settings.bus_2_name = 'Optical Input'
|
|
181
|
+
# Enable Bluetooth
|
|
182
|
+
vssl.settings.bluetooth = True
|
|
183
|
+
# Toggle Bluetooth
|
|
184
|
+
vssl.settings.bluetooth_toggle()
|
|
141
185
|
```
|
|
142
186
|
|
|
143
187
|
## `Vssl.settings.power`
|
|
@@ -225,7 +269,7 @@ zone1.transport.state = ZoneTransport.States.PAUSE
|
|
|
225
269
|
## `Zone.track`
|
|
226
270
|
|
|
227
271
|
* Not all sources have complete metadata - missing value will be set to defaults.
|
|
228
|
-
* Airplay track `progress` is not
|
|
272
|
+
* Airplay track `progress` is not available.
|
|
229
273
|
|
|
230
274
|
| Property | Description | Type | Values |
|
|
231
275
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
@@ -244,15 +288,25 @@ zone1.transport.state = ZoneTransport.States.PAUSE
|
|
|
244
288
|
|
|
245
289
|
| Property | Description | Type | Values |
|
|
246
290
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
247
|
-
| `source` | Change input source | `int` | `InputRouter.Sources`
|
|
248
|
-
| `priority` | Change input priority
|
|
291
|
+
| `source` | Change input source.<br/>Source to be played out the zones speakers | `int` | `InputRouter.Sources`
|
|
292
|
+
| `priority` | Change input priority.<br/>Stream or local to have precedence | `int` | `InputRouter.Priorities`
|
|
293
|
+
|
|
249
294
|
|
|
250
|
-
###
|
|
295
|
+
### A.1(x) Source Routing Order
|
|
296
|
+
A.1 and A.1x don't support manually changing the input source. Instead a fixed source routing order is used:
|
|
297
|
+
|
|
298
|
+
1. Optical Input
|
|
299
|
+
2. Coaxial Input
|
|
300
|
+
3. Analog Input
|
|
301
|
+
|
|
302
|
+
Input `InputRouter.Priorities` still apply.
|
|
303
|
+
|
|
304
|
+
### Input Priority / Precedence
|
|
251
305
|
|
|
252
306
|
| `InputRouter.Priorities` | Priority Order |
|
|
253
307
|
| ---------------------- | ----------- |
|
|
254
|
-
| **`STREAM`** | <ol><li>Stream</li><li>Party Zone</li><li>Bus 1
|
|
255
|
-
| **`LOCAL`** | <ol><li>Bus 1
|
|
308
|
+
| **`STREAM`** | <ol><li>Stream</li><li>Party Zone</li><li>Bus 1</li><li>Bus 2</li><li>Optical</li><li>Coaxial</li><li>Analog</li></ol>
|
|
309
|
+
| **`LOCAL`** | <ol><li>Bus 1</li><li>Bus 2</li><li>Optical</li><li>Coaxial</li><li>Analog</li><li>Stream</li><li>Party Zone</li></ol>
|
|
256
310
|
|
|
257
311
|
```python
|
|
258
312
|
"""Example"""
|
|
@@ -265,7 +319,7 @@ zone1.input.priority = InputRouter.Priorities.LOCAL
|
|
|
265
319
|
|
|
266
320
|
## `Zone.group`
|
|
267
321
|
|
|
268
|
-
|
|
322
|
+
Officially unsupported in X series amplifiers.
|
|
269
323
|
|
|
270
324
|
| Property | Description | Type | Values |
|
|
271
325
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
@@ -275,6 +329,7 @@ Working on A.3x but offically unsupported in x series amplifiers.
|
|
|
275
329
|
| `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
|
|
276
330
|
| `dissolve()` | Dissolve group / remove all members | `func` |
|
|
277
331
|
| `leave()` | Leave the group if a member | `func` |
|
|
332
|
+
| `is_party_zone_member` | Member of Party Zone | `bool` |
|
|
278
333
|
|
|
279
334
|
```python
|
|
280
335
|
"""Examples"""
|
|
@@ -285,13 +340,17 @@ zone2.group.leave() # or
|
|
|
285
340
|
zone1.group.remove_member(ZoneIDs.ZONE_2)
|
|
286
341
|
# If zone 1 is a master, remove all members
|
|
287
342
|
zone1.group.dissolve()
|
|
343
|
+
# Add zone to the party zone group
|
|
344
|
+
zone1.group.is_party_zone_member = True
|
|
345
|
+
# Toggle Party Zone Membership
|
|
346
|
+
zone1.group.is_party_zone_member_toggle()
|
|
288
347
|
```
|
|
289
348
|
|
|
290
349
|
## `Zone.analog_output`
|
|
291
350
|
|
|
292
351
|
| Property | Description | Type | Values | Default |
|
|
293
352
|
| ---------------------- | ----------- | ----------- |----------- |----------- |
|
|
294
|
-
| `source` | Where the AO is routed from. i.e
|
|
353
|
+
| `source` | Where the AO is routed from. i.e stream, optical input or off | `int` | `AnalogOutput.Sources` | `Off`
|
|
295
354
|
| `is_fixed_volume` | Fix the output volume. Output wont respond to volume control | `bool` | |`False`
|
|
296
355
|
| `is_fixed_volume_toggle()` | Toggle fixed volume | `func` |
|
|
297
356
|
|
|
@@ -384,6 +443,23 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
|
|
|
384
443
|
zone1.settings.eq.khz1_db = -2
|
|
385
444
|
```
|
|
386
445
|
|
|
446
|
+
## `Zone.settings.subwoofer`
|
|
447
|
+
|
|
448
|
+
* **A.1 and A.1x only**
|
|
449
|
+
* Set `0` for full frequency range
|
|
450
|
+
|
|
451
|
+
| Property | Description | Type | Values | Default |
|
|
452
|
+
| ---------------------- | ----------- | ----------- |----------- |----------- |
|
|
453
|
+
| `crossover` | Set "sub out" crossover frequency from 50-200Hz. | `int` | `0` or `50...200` | `0`
|
|
454
|
+
|
|
455
|
+
```python
|
|
456
|
+
"""Examples"""
|
|
457
|
+
# Set subwoofer ouput crossover to 100hz
|
|
458
|
+
zone1.settings.subwoofer.crossover = 100
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
|
|
387
463
|
## Another (Lite) Way
|
|
388
464
|
|
|
389
465
|
If you perfer to not run the complete intergration, you can send basic HEX commands to the VSSL device using [Netcat](https://nc110.sourceforge.io/) (or any network tool) on port `50002`.
|
|
@@ -433,9 +509,11 @@ shell_command:
|
|
|
433
509
|
|
|
434
510
|
## Credit
|
|
435
511
|
|
|
512
|
+
Thanks to [@dj-jam](https://github.com/dj-jam) for the continued testing.
|
|
513
|
+
|
|
436
514
|
The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS app and their deprecated [vsslagent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start).
|
|
437
515
|
|
|
438
|
-
|
|
516
|
+
Motivation for this project was to integrate VSSLs amplifiers into [Home Assistant](https://www.home-assistant.io/) and have control over different subnets (not mDNS dependant)
|
|
439
517
|
|
|
440
518
|
## Known Issues & Limitiations
|
|
441
519
|
|
|
@@ -443,15 +521,15 @@ Motovation for this project was to intergrate VSSLs amplifiers into [Home Assist
|
|
|
443
521
|
* VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
|
|
444
522
|
* Not all sources set the volume to 0 when the zone is muted
|
|
445
523
|
* Grouping feedback is flaky on the X series amplifiers
|
|
446
|
-
* Airplay `Zone.track.progress` is not
|
|
524
|
+
* Airplay `Zone.track.progress` is not available.
|
|
447
525
|
* Cant stop a URL playback, feedback is worng at least
|
|
448
526
|
* VSSL likes to cache old track metadata. For example when playing a URL after Spotify, often the device will respond with the previous (Spotify) tracks metadata
|
|
449
|
-
* `stop()` is intended to disconnect the client and pause the stream.
|
|
527
|
+
* `stop()` is intended to disconnect the client and pause the stream. Doesn’t always function this way, depending on stream source
|
|
450
528
|
* Occasionally a zones might stop responding to certain commands, issuing the `reboot` command generally corrects
|
|
451
529
|
|
|
452
530
|
## Future
|
|
453
531
|
|
|
454
|
-
* A.1(x) coverage i.e Bluetooth
|
|
532
|
+
* A.1(x) coverage i.e Bluetooth
|
|
455
533
|
* REST API / Web App
|
|
456
534
|
* Save and recall EQ
|
|
457
535
|
* IR Control
|
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
I am looking for testers with any VSSL amplifier models, please get in touch if you interested in helping. <vsslcontrolled@proton.me>
|
|
10
10
|
|
|
11
11
|
Tested on:
|
|
12
|
-
-
|
|
13
|
-
|
|
12
|
+
- **A.3x** software version **p15305.016.3701**
|
|
13
|
+
|
|
14
|
+
Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) with basic functionality working on:
|
|
15
|
+
- **A.1** software version **p15265.033.3703** (reported)
|
|
16
|
+
- **A.3** software version **p12013.141.3703** (reported)
|
|
17
|
+
- **A.3x** software version **p15305.016.3701**
|
|
18
|
+
- **A.6x** software version **p15305.017.3701** (reported)
|
|
14
19
|
|
|
15
20
|
## Important
|
|
16
21
|
|
|
@@ -18,52 +23,80 @@ There should not be any *[VSSL Agent's](https://vssl.gitbook.io/vssl-rest-api/ge
|
|
|
18
23
|
|
|
19
24
|
## TODOs
|
|
20
25
|
|
|
21
|
-
*
|
|
26
|
+
* Correct IO mapping between models and versions
|
|
22
27
|
* Better test coverage
|
|
28
|
+
* A.1(x) testing
|
|
23
29
|
|
|
24
30
|
## Basic Usage
|
|
25
31
|
|
|
26
32
|
`vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
|
|
27
33
|
|
|
34
|
+
### A.1 Example
|
|
35
|
+
|
|
28
36
|
```python
|
|
29
37
|
import asyncio
|
|
30
38
|
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
31
39
|
|
|
32
40
|
async def main():
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
|
|
42
|
+
# Represents a physical VSSL amplifier
|
|
43
|
+
vssl = Vssl(DeviceModels.A1)
|
|
44
|
+
a1 = vssl.add_zone(ZoneIDs.A1, '192.168.1.10')
|
|
45
|
+
|
|
46
|
+
# Connect and initiate zones.
|
|
47
|
+
await vssl.initialise()
|
|
48
|
+
|
|
49
|
+
"""Control Examples"""
|
|
50
|
+
# Print device name
|
|
51
|
+
print(a1.settings.name)
|
|
52
|
+
# Set volume to 25%
|
|
53
|
+
a1.volume = 25
|
|
54
|
+
# Pause
|
|
55
|
+
a1.pause()
|
|
56
|
+
# Print track name
|
|
57
|
+
print(a1.track.name)
|
|
58
|
+
|
|
59
|
+
# Shutdown and disconnect all zones
|
|
60
|
+
await vssl.shutdown()
|
|
36
61
|
|
|
37
|
-
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
38
64
|
|
|
39
|
-
|
|
65
|
+
### A.3x Example
|
|
40
66
|
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
```python
|
|
68
|
+
import asyncio
|
|
69
|
+
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
70
|
+
|
|
71
|
+
async def main():
|
|
72
|
+
|
|
73
|
+
# Represents a physical VSSL amplifier
|
|
74
|
+
# If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
|
|
75
|
+
vssl = Vssl(DeviceModels.A3X)
|
|
43
76
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
77
|
+
# Add each you wish to control
|
|
78
|
+
zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
|
|
79
|
+
zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
|
|
80
|
+
zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
|
|
81
|
+
#... up to 6 zones
|
|
49
82
|
|
|
50
|
-
|
|
51
|
-
|
|
83
|
+
# Connect and initiate zones.
|
|
84
|
+
await vssl.initialise()
|
|
52
85
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
"""Control Examples"""
|
|
87
|
+
# Print zone1 name
|
|
88
|
+
print(zone1.settings.name)
|
|
89
|
+
# Set zone2 volume to 25%
|
|
90
|
+
zone2.volume = 25
|
|
91
|
+
# Pause zone3
|
|
92
|
+
zone3.pause()
|
|
93
|
+
# or zone3.transport.pause()
|
|
94
|
+
# Print zone1 track name
|
|
95
|
+
print(zone1.track.name)
|
|
63
96
|
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
|
|
98
|
+
# Shutdown and disconnect all zones
|
|
99
|
+
await vssl.shutdown()
|
|
67
100
|
|
|
68
101
|
|
|
69
102
|
asyncio.run(main())
|
|
@@ -86,7 +119,7 @@ print(zone_name)
|
|
|
86
119
|
>>> 'Living Room'
|
|
87
120
|
```
|
|
88
121
|
|
|
89
|
-
**Important** in the above example, `zone1.settings.name` wont be set to its new value until after the VSSL device has changed the name and the `Zone` class has received
|
|
122
|
+
**Important** in the above example, `zone1.settings.name` wont be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events.
|
|
90
123
|
|
|
91
124
|
|
|
92
125
|
# `Vssl`
|
|
@@ -97,26 +130,37 @@ print(zone_name)
|
|
|
97
130
|
| `serial` | Serial number | `str` readonly
|
|
98
131
|
| `model` | Device Model | `int` readonly
|
|
99
132
|
| `reboot()` | Reboot all zones | `func` |
|
|
133
|
+
| `factory_reset()` | Factory reset device | `func` |
|
|
134
|
+
|
|
100
135
|
|
|
101
136
|
```python
|
|
102
137
|
"""Example"""
|
|
103
138
|
# Reboot all zones
|
|
104
139
|
vssl.reboot()
|
|
140
|
+
# Do a factory reset (reset all settings)
|
|
141
|
+
vssl.factory_reset()
|
|
105
142
|
```
|
|
106
143
|
|
|
107
144
|
## `Vssl.settings`
|
|
108
145
|
|
|
109
|
-
| Property | Description | Type |
|
|
110
|
-
| ---------------------- | ----------- | ----------- |
|
|
111
|
-
| `name` | Device name | `str`
|
|
112
|
-
| `
|
|
146
|
+
| Property | Description | Type | Notes / Defaults |
|
|
147
|
+
| ---------------------- | ----------- | ----------- | ----------- |
|
|
148
|
+
| `name` | Device name | `str` |
|
|
149
|
+
| `bus_1_name` | Name of Bus 1 | `str` | <ul><li>A.1: Optical Input</li><li>A.3x/6x: Not Used</li></ul>
|
|
150
|
+
| `bus_2_name` | Name of Bus 2 | `str` | <ul><li>A.1: Coax Input</li><li>A.3x/6x: Optical Input</li></ul>
|
|
151
|
+
| `bluetooth` | Bluetooth enabled / disabled | `bool` |
|
|
152
|
+
| `bluetooth_toggle()` | Toggle Bluetooth | `func` |
|
|
113
153
|
|
|
114
154
|
```python
|
|
115
155
|
"""Example"""
|
|
116
156
|
# Setting device name
|
|
117
157
|
vssl.settings.name = 'My House'
|
|
118
|
-
# Setting
|
|
119
|
-
vssl.settings.
|
|
158
|
+
# Setting bus 2 name
|
|
159
|
+
vssl.settings.bus_2_name = 'Optical Input'
|
|
160
|
+
# Enable Bluetooth
|
|
161
|
+
vssl.settings.bluetooth = True
|
|
162
|
+
# Toggle Bluetooth
|
|
163
|
+
vssl.settings.bluetooth_toggle()
|
|
120
164
|
```
|
|
121
165
|
|
|
122
166
|
## `Vssl.settings.power`
|
|
@@ -204,7 +248,7 @@ zone1.transport.state = ZoneTransport.States.PAUSE
|
|
|
204
248
|
## `Zone.track`
|
|
205
249
|
|
|
206
250
|
* Not all sources have complete metadata - missing value will be set to defaults.
|
|
207
|
-
* Airplay track `progress` is not
|
|
251
|
+
* Airplay track `progress` is not available.
|
|
208
252
|
|
|
209
253
|
| Property | Description | Type | Values |
|
|
210
254
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
@@ -223,15 +267,25 @@ zone1.transport.state = ZoneTransport.States.PAUSE
|
|
|
223
267
|
|
|
224
268
|
| Property | Description | Type | Values |
|
|
225
269
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
226
|
-
| `source` | Change input source | `int` | `InputRouter.Sources`
|
|
227
|
-
| `priority` | Change input priority
|
|
270
|
+
| `source` | Change input source.<br/>Source to be played out the zones speakers | `int` | `InputRouter.Sources`
|
|
271
|
+
| `priority` | Change input priority.<br/>Stream or local to have precedence | `int` | `InputRouter.Priorities`
|
|
272
|
+
|
|
228
273
|
|
|
229
|
-
###
|
|
274
|
+
### A.1(x) Source Routing Order
|
|
275
|
+
A.1 and A.1x don't support manually changing the input source. Instead a fixed source routing order is used:
|
|
276
|
+
|
|
277
|
+
1. Optical Input
|
|
278
|
+
2. Coaxial Input
|
|
279
|
+
3. Analog Input
|
|
280
|
+
|
|
281
|
+
Input `InputRouter.Priorities` still apply.
|
|
282
|
+
|
|
283
|
+
### Input Priority / Precedence
|
|
230
284
|
|
|
231
285
|
| `InputRouter.Priorities` | Priority Order |
|
|
232
286
|
| ---------------------- | ----------- |
|
|
233
|
-
| **`STREAM`** | <ol><li>Stream</li><li>Party Zone</li><li>Bus 1
|
|
234
|
-
| **`LOCAL`** | <ol><li>Bus 1
|
|
287
|
+
| **`STREAM`** | <ol><li>Stream</li><li>Party Zone</li><li>Bus 1</li><li>Bus 2</li><li>Optical</li><li>Coaxial</li><li>Analog</li></ol>
|
|
288
|
+
| **`LOCAL`** | <ol><li>Bus 1</li><li>Bus 2</li><li>Optical</li><li>Coaxial</li><li>Analog</li><li>Stream</li><li>Party Zone</li></ol>
|
|
235
289
|
|
|
236
290
|
```python
|
|
237
291
|
"""Example"""
|
|
@@ -244,7 +298,7 @@ zone1.input.priority = InputRouter.Priorities.LOCAL
|
|
|
244
298
|
|
|
245
299
|
## `Zone.group`
|
|
246
300
|
|
|
247
|
-
|
|
301
|
+
Officially unsupported in X series amplifiers.
|
|
248
302
|
|
|
249
303
|
| Property | Description | Type | Values |
|
|
250
304
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
@@ -254,6 +308,7 @@ Working on A.3x but offically unsupported in x series amplifiers.
|
|
|
254
308
|
| `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
|
|
255
309
|
| `dissolve()` | Dissolve group / remove all members | `func` |
|
|
256
310
|
| `leave()` | Leave the group if a member | `func` |
|
|
311
|
+
| `is_party_zone_member` | Member of Party Zone | `bool` |
|
|
257
312
|
|
|
258
313
|
```python
|
|
259
314
|
"""Examples"""
|
|
@@ -264,13 +319,17 @@ zone2.group.leave() # or
|
|
|
264
319
|
zone1.group.remove_member(ZoneIDs.ZONE_2)
|
|
265
320
|
# If zone 1 is a master, remove all members
|
|
266
321
|
zone1.group.dissolve()
|
|
322
|
+
# Add zone to the party zone group
|
|
323
|
+
zone1.group.is_party_zone_member = True
|
|
324
|
+
# Toggle Party Zone Membership
|
|
325
|
+
zone1.group.is_party_zone_member_toggle()
|
|
267
326
|
```
|
|
268
327
|
|
|
269
328
|
## `Zone.analog_output`
|
|
270
329
|
|
|
271
330
|
| Property | Description | Type | Values | Default |
|
|
272
331
|
| ---------------------- | ----------- | ----------- |----------- |----------- |
|
|
273
|
-
| `source` | Where the AO is routed from. i.e
|
|
332
|
+
| `source` | Where the AO is routed from. i.e stream, optical input or off | `int` | `AnalogOutput.Sources` | `Off`
|
|
274
333
|
| `is_fixed_volume` | Fix the output volume. Output wont respond to volume control | `bool` | |`False`
|
|
275
334
|
| `is_fixed_volume_toggle()` | Toggle fixed volume | `func` |
|
|
276
335
|
|
|
@@ -363,6 +422,23 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
|
|
|
363
422
|
zone1.settings.eq.khz1_db = -2
|
|
364
423
|
```
|
|
365
424
|
|
|
425
|
+
## `Zone.settings.subwoofer`
|
|
426
|
+
|
|
427
|
+
* **A.1 and A.1x only**
|
|
428
|
+
* Set `0` for full frequency range
|
|
429
|
+
|
|
430
|
+
| Property | Description | Type | Values | Default |
|
|
431
|
+
| ---------------------- | ----------- | ----------- |----------- |----------- |
|
|
432
|
+
| `crossover` | Set "sub out" crossover frequency from 50-200Hz. | `int` | `0` or `50...200` | `0`
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
"""Examples"""
|
|
436
|
+
# Set subwoofer ouput crossover to 100hz
|
|
437
|
+
zone1.settings.subwoofer.crossover = 100
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
366
442
|
## Another (Lite) Way
|
|
367
443
|
|
|
368
444
|
If you perfer to not run the complete intergration, you can send basic HEX commands to the VSSL device using [Netcat](https://nc110.sourceforge.io/) (or any network tool) on port `50002`.
|
|
@@ -412,9 +488,11 @@ shell_command:
|
|
|
412
488
|
|
|
413
489
|
## Credit
|
|
414
490
|
|
|
491
|
+
Thanks to [@dj-jam](https://github.com/dj-jam) for the continued testing.
|
|
492
|
+
|
|
415
493
|
The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS app and their deprecated [vsslagent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start).
|
|
416
494
|
|
|
417
|
-
|
|
495
|
+
Motivation for this project was to integrate VSSLs amplifiers into [Home Assistant](https://www.home-assistant.io/) and have control over different subnets (not mDNS dependant)
|
|
418
496
|
|
|
419
497
|
## Known Issues & Limitiations
|
|
420
498
|
|
|
@@ -422,15 +500,15 @@ Motovation for this project was to intergrate VSSLs amplifiers into [Home Assist
|
|
|
422
500
|
* VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
|
|
423
501
|
* Not all sources set the volume to 0 when the zone is muted
|
|
424
502
|
* Grouping feedback is flaky on the X series amplifiers
|
|
425
|
-
* Airplay `Zone.track.progress` is not
|
|
503
|
+
* Airplay `Zone.track.progress` is not available.
|
|
426
504
|
* Cant stop a URL playback, feedback is worng at least
|
|
427
505
|
* VSSL likes to cache old track metadata. For example when playing a URL after Spotify, often the device will respond with the previous (Spotify) tracks metadata
|
|
428
|
-
* `stop()` is intended to disconnect the client and pause the stream.
|
|
506
|
+
* `stop()` is intended to disconnect the client and pause the stream. Doesn’t always function this way, depending on stream source
|
|
429
507
|
* Occasionally a zones might stop responding to certain commands, issuing the `reboot` command generally corrects
|
|
430
508
|
|
|
431
509
|
## Future
|
|
432
510
|
|
|
433
|
-
* A.1(x) coverage i.e Bluetooth
|
|
511
|
+
* A.1(x) coverage i.e Bluetooth
|
|
434
512
|
* REST API / Web App
|
|
435
513
|
* Save and recall EQ
|
|
436
514
|
* IR Control
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vsslctrl"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.11.dev1"
|
|
8
8
|
description = "Package for controlling VSSL's range of streaming amplifiers"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="vsslctrl", email="vsslcontrolled@proton.me" },
|