vsslctrl 0.1.8.dev1__tar.gz → 0.1.10.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.8.dev1 → vsslctrl-0.1.10.dev1}/PKG-INFO +100 -37
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/README.md +99 -36
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/pyproject.toml +1 -1
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/__init__.py +3 -2
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/api_alpha.py +57 -9
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/core.py +20 -2
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/data_structure.py +74 -17
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/device.py +45 -29
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/group.py +30 -4
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/io.py +13 -7
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/settings.py +13 -8
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl.egg-info/PKG-INFO +100 -37
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/LICENSE +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/setup.cfg +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/tests/test_event_bus.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/tests/test_integration.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/tests/test_settings.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/tests/test_zone.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/api_base.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/api_bravo.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/decorators.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/discovery.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/event_bus.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/exceptions.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/track.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/transport.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/utils.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/zone.py +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl.egg-info/SOURCES.txt +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl.egg-info/dependency_links.txt +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl.egg-info/requires.txt +0 -0
- {vsslctrl-0.1.8.dev1 → vsslctrl-0.1.10.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.10.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,10 @@ 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.1** software version **p15265.033.3703** via Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) (reported)
|
|
34
|
+
- **A.3** software version **p12013.141.3703** via Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) (reported)
|
|
35
|
+
- **A.3x** software version **p15305.016.3701**
|
|
36
|
+
- **A.6x** software version **p15305.017.3701** via Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) (reported)
|
|
35
37
|
|
|
36
38
|
## Important
|
|
37
39
|
|
|
@@ -39,52 +41,79 @@ There should not be any *[VSSL Agent's](https://vssl.gitbook.io/vssl-rest-api/ge
|
|
|
39
41
|
|
|
40
42
|
## TODOs
|
|
41
43
|
|
|
42
|
-
* **
|
|
44
|
+
* **A.1(x)** specific control e.g bluetooth
|
|
43
45
|
* Better test coverage
|
|
44
46
|
|
|
45
47
|
## Basic Usage
|
|
46
48
|
|
|
47
49
|
`vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
|
|
48
50
|
|
|
51
|
+
### A.1 Example
|
|
52
|
+
|
|
49
53
|
```python
|
|
50
54
|
import asyncio
|
|
51
55
|
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
52
56
|
|
|
53
57
|
async def main():
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
|
|
59
|
+
# Represents a physical VSSL amplifier
|
|
60
|
+
vssl = Vssl(DeviceModels.A1)
|
|
61
|
+
a1 = vssl.add_zone(ZoneIDs.A1, '192.168.1.10')
|
|
62
|
+
|
|
63
|
+
# Connect and initiate zones.
|
|
64
|
+
await vssl.initialise()
|
|
65
|
+
|
|
66
|
+
"""Control Examples"""
|
|
67
|
+
# Print device name
|
|
68
|
+
print(a1.settings.name)
|
|
69
|
+
# Set volume to 25%
|
|
70
|
+
a1.volume = 25
|
|
71
|
+
# Pause
|
|
72
|
+
a1.pause()
|
|
73
|
+
# Print track name
|
|
74
|
+
print(a1.track.name)
|
|
75
|
+
|
|
76
|
+
# Shutdown and disconnect all zones
|
|
77
|
+
await vssl.shutdown()
|
|
57
78
|
|
|
58
|
-
|
|
79
|
+
asyncio.run(main())
|
|
80
|
+
```
|
|
59
81
|
|
|
60
|
-
|
|
82
|
+
### A.3x Example
|
|
61
83
|
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
```python
|
|
85
|
+
import asyncio
|
|
86
|
+
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
64
87
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
async def main():
|
|
89
|
+
|
|
90
|
+
# Represents a physical VSSL amplifier
|
|
91
|
+
# If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
|
|
92
|
+
vssl = Vssl(DeviceModels.A3X)
|
|
70
93
|
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
# Add each you wish to control
|
|
95
|
+
zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
|
|
96
|
+
zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
|
|
97
|
+
zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
|
|
98
|
+
#... up to 6 zones
|
|
73
99
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
print(zone1.settings.name)
|
|
77
|
-
# Set zone2 volume to 25%
|
|
78
|
-
zone2.volume = 25
|
|
79
|
-
# Pause zone3
|
|
80
|
-
zone3.pause()
|
|
81
|
-
# or zone3.transport.pause()
|
|
82
|
-
# Print zone1 track name
|
|
83
|
-
print(zone1.track.name)
|
|
100
|
+
# Connect and initiate zones.
|
|
101
|
+
await vssl.initialise()
|
|
84
102
|
|
|
103
|
+
"""Control Examples"""
|
|
104
|
+
# Print zone1 name
|
|
105
|
+
print(zone1.settings.name)
|
|
106
|
+
# Set zone2 volume to 25%
|
|
107
|
+
zone2.volume = 25
|
|
108
|
+
# Pause zone3
|
|
109
|
+
zone3.pause()
|
|
110
|
+
# or zone3.transport.pause()
|
|
111
|
+
# Print zone1 track name
|
|
112
|
+
print(zone1.track.name)
|
|
85
113
|
|
|
86
|
-
|
|
87
|
-
|
|
114
|
+
|
|
115
|
+
# Shutdown and disconnect all zones
|
|
116
|
+
await vssl.shutdown()
|
|
88
117
|
|
|
89
118
|
|
|
90
119
|
asyncio.run(main())
|
|
@@ -107,7 +136,7 @@ print(zone_name)
|
|
|
107
136
|
>>> 'Living Room'
|
|
108
137
|
```
|
|
109
138
|
|
|
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
|
|
139
|
+
**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
140
|
|
|
112
141
|
|
|
113
142
|
# `Vssl`
|
|
@@ -118,11 +147,15 @@ print(zone_name)
|
|
|
118
147
|
| `serial` | Serial number | `str` readonly
|
|
119
148
|
| `model` | Device Model | `int` readonly
|
|
120
149
|
| `reboot()` | Reboot all zones | `func` |
|
|
150
|
+
| `factory_reset()` | Factory reset device | `func` |
|
|
151
|
+
|
|
121
152
|
|
|
122
153
|
```python
|
|
123
154
|
"""Example"""
|
|
124
155
|
# Reboot all zones
|
|
125
156
|
vssl.reboot()
|
|
157
|
+
# Do a factory reset (reset all settings)
|
|
158
|
+
vssl.factory_reset()
|
|
126
159
|
```
|
|
127
160
|
|
|
128
161
|
## `Vssl.settings`
|
|
@@ -131,6 +164,8 @@ vssl.reboot()
|
|
|
131
164
|
| ---------------------- | ----------- | ----------- |
|
|
132
165
|
| `name` | Device name | `str`
|
|
133
166
|
| `optical_input_name` | Name of the optical input | `str`
|
|
167
|
+
| `bluetooth` | Bluetooth enabled / disabled | `bool`
|
|
168
|
+
| `bluetooth_toggle()` | Toggle Bluetooth | `func` |
|
|
134
169
|
|
|
135
170
|
```python
|
|
136
171
|
"""Example"""
|
|
@@ -138,6 +173,10 @@ vssl.reboot()
|
|
|
138
173
|
vssl.settings.name = 'My House'
|
|
139
174
|
# Setting optical input name
|
|
140
175
|
vssl.settings.optical_input_name = 'Optical Input 1'
|
|
176
|
+
# Enable Bluetooth
|
|
177
|
+
vssl.settings.bluetooth = True
|
|
178
|
+
# Toggle Bluetooth
|
|
179
|
+
vssl.settings.bluetooth_toggle()
|
|
141
180
|
```
|
|
142
181
|
|
|
143
182
|
## `Vssl.settings.power`
|
|
@@ -225,7 +264,7 @@ zone1.transport.state = ZoneTransport.States.PAUSE
|
|
|
225
264
|
## `Zone.track`
|
|
226
265
|
|
|
227
266
|
* Not all sources have complete metadata - missing value will be set to defaults.
|
|
228
|
-
* Airplay track `progress` is not
|
|
267
|
+
* Airplay track `progress` is not available.
|
|
229
268
|
|
|
230
269
|
| Property | Description | Type | Values |
|
|
231
270
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
@@ -265,7 +304,7 @@ zone1.input.priority = InputRouter.Priorities.LOCAL
|
|
|
265
304
|
|
|
266
305
|
## `Zone.group`
|
|
267
306
|
|
|
268
|
-
|
|
307
|
+
Officially unsupported in X series amplifiers.
|
|
269
308
|
|
|
270
309
|
| Property | Description | Type | Values |
|
|
271
310
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
@@ -275,6 +314,7 @@ Working on A.3x but offically unsupported in x series amplifiers.
|
|
|
275
314
|
| `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
|
|
276
315
|
| `dissolve()` | Dissolve group / remove all members | `func` |
|
|
277
316
|
| `leave()` | Leave the group if a member | `func` |
|
|
317
|
+
| `is_party_zone_member` | Member of Party Zone | `bool` |
|
|
278
318
|
|
|
279
319
|
```python
|
|
280
320
|
"""Examples"""
|
|
@@ -285,6 +325,10 @@ zone2.group.leave() # or
|
|
|
285
325
|
zone1.group.remove_member(ZoneIDs.ZONE_2)
|
|
286
326
|
# If zone 1 is a master, remove all members
|
|
287
327
|
zone1.group.dissolve()
|
|
328
|
+
# Add zone to the party zone group
|
|
329
|
+
zone1.group.is_party_zone_member = True
|
|
330
|
+
# Toggle Party Zone Membership
|
|
331
|
+
zone1.group.is_party_zone_member_toggle()
|
|
288
332
|
```
|
|
289
333
|
|
|
290
334
|
## `Zone.analog_output`
|
|
@@ -384,6 +428,23 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
|
|
|
384
428
|
zone1.settings.eq.khz1_db = -2
|
|
385
429
|
```
|
|
386
430
|
|
|
431
|
+
## `Zone.settings.subwoofer`
|
|
432
|
+
|
|
433
|
+
* **A.1 and A.1x only**
|
|
434
|
+
* Set `0` for full frequency range
|
|
435
|
+
|
|
436
|
+
| Property | Description | Type | Values | Default |
|
|
437
|
+
| ---------------------- | ----------- | ----------- |----------- |----------- |
|
|
438
|
+
| `crossover` | Set "sub out" crossover frequency from 50-200Hz. | `int` | `0` or `50...200` | `0`
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
"""Examples"""
|
|
442
|
+
# Set subwoofer ouput crossover to 100hz
|
|
443
|
+
zone1.settings.subwoofer.crossover = 100
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
387
448
|
## Another (Lite) Way
|
|
388
449
|
|
|
389
450
|
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 +494,11 @@ shell_command:
|
|
|
433
494
|
|
|
434
495
|
## Credit
|
|
435
496
|
|
|
497
|
+
Thanks to [@dj-jam](https://github.com/dj-jam) for the continued testing.
|
|
498
|
+
|
|
436
499
|
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
500
|
|
|
438
|
-
|
|
501
|
+
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
502
|
|
|
440
503
|
## Known Issues & Limitiations
|
|
441
504
|
|
|
@@ -443,15 +506,15 @@ Motovation for this project was to intergrate VSSLs amplifiers into [Home Assist
|
|
|
443
506
|
* VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
|
|
444
507
|
* Not all sources set the volume to 0 when the zone is muted
|
|
445
508
|
* Grouping feedback is flaky on the X series amplifiers
|
|
446
|
-
* Airplay `Zone.track.progress` is not
|
|
509
|
+
* Airplay `Zone.track.progress` is not available.
|
|
447
510
|
* Cant stop a URL playback, feedback is worng at least
|
|
448
511
|
* 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.
|
|
512
|
+
* `stop()` is intended to disconnect the client and pause the stream. Doesn’t always function this way, depending on stream source
|
|
450
513
|
* Occasionally a zones might stop responding to certain commands, issuing the `reboot` command generally corrects
|
|
451
514
|
|
|
452
515
|
## Future
|
|
453
516
|
|
|
454
|
-
* A.1(x) coverage i.e Bluetooth
|
|
517
|
+
* A.1(x) coverage i.e Bluetooth
|
|
455
518
|
* REST API / Web App
|
|
456
519
|
* Save and recall EQ
|
|
457
520
|
* IR Control
|
|
@@ -9,8 +9,10 @@
|
|
|
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.1** software version **p15265.033.3703** via Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) (reported)
|
|
13
|
+
- **A.3** software version **p12013.141.3703** via Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) (reported)
|
|
14
|
+
- **A.3x** software version **p15305.016.3701**
|
|
15
|
+
- **A.6x** software version **p15305.017.3701** via Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) (reported)
|
|
14
16
|
|
|
15
17
|
## Important
|
|
16
18
|
|
|
@@ -18,52 +20,79 @@ There should not be any *[VSSL Agent's](https://vssl.gitbook.io/vssl-rest-api/ge
|
|
|
18
20
|
|
|
19
21
|
## TODOs
|
|
20
22
|
|
|
21
|
-
* **
|
|
23
|
+
* **A.1(x)** specific control e.g bluetooth
|
|
22
24
|
* Better test coverage
|
|
23
25
|
|
|
24
26
|
## Basic Usage
|
|
25
27
|
|
|
26
28
|
`vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
|
|
27
29
|
|
|
30
|
+
### A.1 Example
|
|
31
|
+
|
|
28
32
|
```python
|
|
29
33
|
import asyncio
|
|
30
34
|
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
31
35
|
|
|
32
36
|
async def main():
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
|
|
38
|
+
# Represents a physical VSSL amplifier
|
|
39
|
+
vssl = Vssl(DeviceModels.A1)
|
|
40
|
+
a1 = vssl.add_zone(ZoneIDs.A1, '192.168.1.10')
|
|
41
|
+
|
|
42
|
+
# Connect and initiate zones.
|
|
43
|
+
await vssl.initialise()
|
|
44
|
+
|
|
45
|
+
"""Control Examples"""
|
|
46
|
+
# Print device name
|
|
47
|
+
print(a1.settings.name)
|
|
48
|
+
# Set volume to 25%
|
|
49
|
+
a1.volume = 25
|
|
50
|
+
# Pause
|
|
51
|
+
a1.pause()
|
|
52
|
+
# Print track name
|
|
53
|
+
print(a1.track.name)
|
|
54
|
+
|
|
55
|
+
# Shutdown and disconnect all zones
|
|
56
|
+
await vssl.shutdown()
|
|
36
57
|
|
|
37
|
-
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
### A.3x Example
|
|
40
62
|
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
```python
|
|
64
|
+
import asyncio
|
|
65
|
+
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
async def main():
|
|
68
|
+
|
|
69
|
+
# Represents a physical VSSL amplifier
|
|
70
|
+
# If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
|
|
71
|
+
vssl = Vssl(DeviceModels.A3X)
|
|
49
72
|
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
# Add each you wish to control
|
|
74
|
+
zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
|
|
75
|
+
zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
|
|
76
|
+
zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
|
|
77
|
+
#... up to 6 zones
|
|
52
78
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
print(zone1.settings.name)
|
|
56
|
-
# Set zone2 volume to 25%
|
|
57
|
-
zone2.volume = 25
|
|
58
|
-
# Pause zone3
|
|
59
|
-
zone3.pause()
|
|
60
|
-
# or zone3.transport.pause()
|
|
61
|
-
# Print zone1 track name
|
|
62
|
-
print(zone1.track.name)
|
|
79
|
+
# Connect and initiate zones.
|
|
80
|
+
await vssl.initialise()
|
|
63
81
|
|
|
82
|
+
"""Control Examples"""
|
|
83
|
+
# Print zone1 name
|
|
84
|
+
print(zone1.settings.name)
|
|
85
|
+
# Set zone2 volume to 25%
|
|
86
|
+
zone2.volume = 25
|
|
87
|
+
# Pause zone3
|
|
88
|
+
zone3.pause()
|
|
89
|
+
# or zone3.transport.pause()
|
|
90
|
+
# Print zone1 track name
|
|
91
|
+
print(zone1.track.name)
|
|
64
92
|
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
|
|
94
|
+
# Shutdown and disconnect all zones
|
|
95
|
+
await vssl.shutdown()
|
|
67
96
|
|
|
68
97
|
|
|
69
98
|
asyncio.run(main())
|
|
@@ -86,7 +115,7 @@ print(zone_name)
|
|
|
86
115
|
>>> 'Living Room'
|
|
87
116
|
```
|
|
88
117
|
|
|
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
|
|
118
|
+
**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
119
|
|
|
91
120
|
|
|
92
121
|
# `Vssl`
|
|
@@ -97,11 +126,15 @@ print(zone_name)
|
|
|
97
126
|
| `serial` | Serial number | `str` readonly
|
|
98
127
|
| `model` | Device Model | `int` readonly
|
|
99
128
|
| `reboot()` | Reboot all zones | `func` |
|
|
129
|
+
| `factory_reset()` | Factory reset device | `func` |
|
|
130
|
+
|
|
100
131
|
|
|
101
132
|
```python
|
|
102
133
|
"""Example"""
|
|
103
134
|
# Reboot all zones
|
|
104
135
|
vssl.reboot()
|
|
136
|
+
# Do a factory reset (reset all settings)
|
|
137
|
+
vssl.factory_reset()
|
|
105
138
|
```
|
|
106
139
|
|
|
107
140
|
## `Vssl.settings`
|
|
@@ -110,6 +143,8 @@ vssl.reboot()
|
|
|
110
143
|
| ---------------------- | ----------- | ----------- |
|
|
111
144
|
| `name` | Device name | `str`
|
|
112
145
|
| `optical_input_name` | Name of the optical input | `str`
|
|
146
|
+
| `bluetooth` | Bluetooth enabled / disabled | `bool`
|
|
147
|
+
| `bluetooth_toggle()` | Toggle Bluetooth | `func` |
|
|
113
148
|
|
|
114
149
|
```python
|
|
115
150
|
"""Example"""
|
|
@@ -117,6 +152,10 @@ vssl.reboot()
|
|
|
117
152
|
vssl.settings.name = 'My House'
|
|
118
153
|
# Setting optical input name
|
|
119
154
|
vssl.settings.optical_input_name = 'Optical Input 1'
|
|
155
|
+
# Enable Bluetooth
|
|
156
|
+
vssl.settings.bluetooth = True
|
|
157
|
+
# Toggle Bluetooth
|
|
158
|
+
vssl.settings.bluetooth_toggle()
|
|
120
159
|
```
|
|
121
160
|
|
|
122
161
|
## `Vssl.settings.power`
|
|
@@ -204,7 +243,7 @@ zone1.transport.state = ZoneTransport.States.PAUSE
|
|
|
204
243
|
## `Zone.track`
|
|
205
244
|
|
|
206
245
|
* Not all sources have complete metadata - missing value will be set to defaults.
|
|
207
|
-
* Airplay track `progress` is not
|
|
246
|
+
* Airplay track `progress` is not available.
|
|
208
247
|
|
|
209
248
|
| Property | Description | Type | Values |
|
|
210
249
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
@@ -244,7 +283,7 @@ zone1.input.priority = InputRouter.Priorities.LOCAL
|
|
|
244
283
|
|
|
245
284
|
## `Zone.group`
|
|
246
285
|
|
|
247
|
-
|
|
286
|
+
Officially unsupported in X series amplifiers.
|
|
248
287
|
|
|
249
288
|
| Property | Description | Type | Values |
|
|
250
289
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
@@ -254,6 +293,7 @@ Working on A.3x but offically unsupported in x series amplifiers.
|
|
|
254
293
|
| `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
|
|
255
294
|
| `dissolve()` | Dissolve group / remove all members | `func` |
|
|
256
295
|
| `leave()` | Leave the group if a member | `func` |
|
|
296
|
+
| `is_party_zone_member` | Member of Party Zone | `bool` |
|
|
257
297
|
|
|
258
298
|
```python
|
|
259
299
|
"""Examples"""
|
|
@@ -264,6 +304,10 @@ zone2.group.leave() # or
|
|
|
264
304
|
zone1.group.remove_member(ZoneIDs.ZONE_2)
|
|
265
305
|
# If zone 1 is a master, remove all members
|
|
266
306
|
zone1.group.dissolve()
|
|
307
|
+
# Add zone to the party zone group
|
|
308
|
+
zone1.group.is_party_zone_member = True
|
|
309
|
+
# Toggle Party Zone Membership
|
|
310
|
+
zone1.group.is_party_zone_member_toggle()
|
|
267
311
|
```
|
|
268
312
|
|
|
269
313
|
## `Zone.analog_output`
|
|
@@ -363,6 +407,23 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
|
|
|
363
407
|
zone1.settings.eq.khz1_db = -2
|
|
364
408
|
```
|
|
365
409
|
|
|
410
|
+
## `Zone.settings.subwoofer`
|
|
411
|
+
|
|
412
|
+
* **A.1 and A.1x only**
|
|
413
|
+
* Set `0` for full frequency range
|
|
414
|
+
|
|
415
|
+
| Property | Description | Type | Values | Default |
|
|
416
|
+
| ---------------------- | ----------- | ----------- |----------- |----------- |
|
|
417
|
+
| `crossover` | Set "sub out" crossover frequency from 50-200Hz. | `int` | `0` or `50...200` | `0`
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
"""Examples"""
|
|
421
|
+
# Set subwoofer ouput crossover to 100hz
|
|
422
|
+
zone1.settings.subwoofer.crossover = 100
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
366
427
|
## Another (Lite) Way
|
|
367
428
|
|
|
368
429
|
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 +473,11 @@ shell_command:
|
|
|
412
473
|
|
|
413
474
|
## Credit
|
|
414
475
|
|
|
476
|
+
Thanks to [@dj-jam](https://github.com/dj-jam) for the continued testing.
|
|
477
|
+
|
|
415
478
|
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
479
|
|
|
417
|
-
|
|
480
|
+
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
481
|
|
|
419
482
|
## Known Issues & Limitiations
|
|
420
483
|
|
|
@@ -422,15 +485,15 @@ Motovation for this project was to intergrate VSSLs amplifiers into [Home Assist
|
|
|
422
485
|
* VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
|
|
423
486
|
* Not all sources set the volume to 0 when the zone is muted
|
|
424
487
|
* Grouping feedback is flaky on the X series amplifiers
|
|
425
|
-
* Airplay `Zone.track.progress` is not
|
|
488
|
+
* Airplay `Zone.track.progress` is not available.
|
|
426
489
|
* Cant stop a URL playback, feedback is worng at least
|
|
427
490
|
* 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.
|
|
491
|
+
* `stop()` is intended to disconnect the client and pause the stream. Doesn’t always function this way, depending on stream source
|
|
429
492
|
* Occasionally a zones might stop responding to certain commands, issuing the `reboot` command generally corrects
|
|
430
493
|
|
|
431
494
|
## Future
|
|
432
495
|
|
|
433
|
-
* A.1(x) coverage i.e Bluetooth
|
|
496
|
+
* A.1(x) coverage i.e Bluetooth
|
|
434
497
|
* REST API / Web App
|
|
435
498
|
* Save and recall EQ
|
|
436
499
|
* 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.10.dev1"
|
|
8
8
|
description = "Package for controlling VSSL's range of streaming amplifiers"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="vsslctrl", email="vsslcontrolled@proton.me" },
|
|
@@ -12,6 +12,7 @@ from .settings import EQSettings
|
|
|
12
12
|
from .utils import hex_to_int, clamp_volume, hex_to_bytearray_string
|
|
13
13
|
from .decorators import logging_helpers
|
|
14
14
|
from .data_structure import (
|
|
15
|
+
ZoneIDs,
|
|
15
16
|
ZoneStatusExtKeys,
|
|
16
17
|
ZoneEQStatusExtKeys,
|
|
17
18
|
ZoneRouterStatusExtKeys,
|
|
@@ -195,6 +196,17 @@ class APIAlpha(APIBase):
|
|
|
195
196
|
self._log_debug("Requesting status transport state")
|
|
196
197
|
self.send(bytearray([16, 7, 1, 0]))
|
|
197
198
|
|
|
199
|
+
#
|
|
200
|
+
# 0C [12]
|
|
201
|
+
# Party Mode
|
|
202
|
+
#
|
|
203
|
+
def request_action_0C(self, state: int):
|
|
204
|
+
self._log_debug(f"Requesting to set party memeber: {state}")
|
|
205
|
+
command = self._add_zone_id_to_request(
|
|
206
|
+
bytearray([16, 11, 2, 0, int(not not state)])
|
|
207
|
+
)
|
|
208
|
+
self.send(command)
|
|
209
|
+
|
|
198
210
|
#
|
|
199
211
|
# 0D [13]
|
|
200
212
|
# EQ
|
|
@@ -383,9 +395,17 @@ class APIAlpha(APIBase):
|
|
|
383
395
|
# Reboot All Zones
|
|
384
396
|
#
|
|
385
397
|
def request_action_33_device(self):
|
|
386
|
-
self._log_debug(
|
|
398
|
+
self._log_debug("Requesting to reboot device")
|
|
387
399
|
self.send(bytearray([16, 51, 2, 0, 1]))
|
|
388
400
|
|
|
401
|
+
#
|
|
402
|
+
# 2B [43]
|
|
403
|
+
# Factory Reset Device
|
|
404
|
+
#
|
|
405
|
+
def request_action_2B(self):
|
|
406
|
+
self._log_debug("Requesting to factory reset device")
|
|
407
|
+
self.send(bytearray([16, 43, 2, 8, 0]))
|
|
408
|
+
|
|
389
409
|
#
|
|
390
410
|
# 47 [71]
|
|
391
411
|
# Set Input Priority
|
|
@@ -481,6 +501,16 @@ class APIAlpha(APIBase):
|
|
|
481
501
|
command = bytearray([16, 79, 2, 8, int(state)])
|
|
482
502
|
self.send(command)
|
|
483
503
|
|
|
504
|
+
#
|
|
505
|
+
# 57 [87]
|
|
506
|
+
# Subwoofer Crossover
|
|
507
|
+
#
|
|
508
|
+
def request_action_57(self, freq: int):
|
|
509
|
+
self._log_debug(f"Requesting to set subwoofer crossover: {freq}")
|
|
510
|
+
# We hard code the zone ID to be 7 since this A.1(x)
|
|
511
|
+
command = bytearray([16, 87, 3, ZoneIDs.A1, 0, freq])
|
|
512
|
+
self.send(command)
|
|
513
|
+
|
|
484
514
|
#
|
|
485
515
|
#
|
|
486
516
|
#
|
|
@@ -581,6 +611,9 @@ class APIAlpha(APIBase):
|
|
|
581
611
|
# Not used?
|
|
582
612
|
|
|
583
613
|
# B2Nm - Bus2 Name - For A3.X this is the optical input name
|
|
614
|
+
# B1Nm - Bus1 Name - For A1 this is the Optical input
|
|
615
|
+
# B2Nm - Bus2 Name - For A1 this is the coax
|
|
616
|
+
|
|
584
617
|
if DeviceStatusExtKeys.OPTICAL_INPUT_NAME in metadata:
|
|
585
618
|
self.vssl.settings._set_property(
|
|
586
619
|
"optical_input_name",
|
|
@@ -662,9 +695,10 @@ class APIAlpha(APIBase):
|
|
|
662
695
|
self.zone._set_property("mute", bool(int(metadata[ZoneStatusExtKeys.MUTE])))
|
|
663
696
|
|
|
664
697
|
# Party Mode
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
698
|
+
if ZoneStatusExtKeys.PARTY_ZONE in metadata:
|
|
699
|
+
self.zone.group._set_property(
|
|
700
|
+
"is_party_zone_member", int(metadata[ZoneStatusExtKeys.PARTY_ZONE])
|
|
701
|
+
)
|
|
668
702
|
|
|
669
703
|
# Group Index see below
|
|
670
704
|
if ZoneStatusExtKeys.GROUP_INDEX in metadata:
|
|
@@ -929,11 +963,15 @@ class APIAlpha(APIBase):
|
|
|
929
963
|
self.zone.transport._set_property("state", state)
|
|
930
964
|
|
|
931
965
|
#
|
|
932
|
-
#
|
|
933
|
-
# Party Mode
|
|
966
|
+
# 0C [12]
|
|
967
|
+
# Party Mode
|
|
934
968
|
#
|
|
935
|
-
|
|
936
|
-
|
|
969
|
+
# Not supported on X series
|
|
970
|
+
#
|
|
971
|
+
def response_action_0C(self, hexl: list, response: bytes):
|
|
972
|
+
state = hex_to_int(hexl[4])
|
|
973
|
+
self._log_debug(f"Received party member state: {state}")
|
|
974
|
+
self.zone.group._set_property("is_party_zone_member", state)
|
|
937
975
|
|
|
938
976
|
#
|
|
939
977
|
# 0E [14]
|
|
@@ -943,7 +981,7 @@ class APIAlpha(APIBase):
|
|
|
943
981
|
if hex_to_int(hexl[2]) == 3:
|
|
944
982
|
freq = hex_to_int(hexl[4])
|
|
945
983
|
value = hex_to_int(hexl[5])
|
|
946
|
-
self._log_debug(f"Received EQ
|
|
984
|
+
self._log_debug(f"Received EQ frequency:{freq} value: {value}")
|
|
947
985
|
self.zone.settings.eq._set_eq_freq(freq, value)
|
|
948
986
|
|
|
949
987
|
#
|
|
@@ -1087,6 +1125,16 @@ class APIAlpha(APIBase):
|
|
|
1087
1125
|
self._log_debug(f"Received adaptive power setting: {enabled}")
|
|
1088
1126
|
self.vssl.settings.power._set_property("adaptive", bool(int(enabled)))
|
|
1089
1127
|
|
|
1128
|
+
#
|
|
1129
|
+
# 58 [88]
|
|
1130
|
+
# Subwoofer Crossover Feedback
|
|
1131
|
+
#
|
|
1132
|
+
def response_action_58(self, hexl: list, response: bytes):
|
|
1133
|
+
if hex_to_int(hexl[2]) == 3:
|
|
1134
|
+
freq = hex_to_int(hexl[5])
|
|
1135
|
+
self._log_debug(f"Received subwoofer crossover frequency: {freq}")
|
|
1136
|
+
self.zone.settings.subwoofer._set_property("crossover", freq)
|
|
1137
|
+
|
|
1090
1138
|
#
|
|
1091
1139
|
# Command confimation
|
|
1092
1140
|
#
|