vsslctrl 0.1.13.dev1__tar.gz → 0.2.0__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.13.dev1 → vsslctrl-0.2.0}/PKG-INFO +35 -70
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/README.md +32 -68
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/pyproject.toml +1 -1
- vsslctrl-0.2.0/tests/test_api.py +64 -0
- vsslctrl-0.2.0/tests/test_device.py +87 -0
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/tests/test_event_bus.py +5 -4
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/tests/test_integration.py +116 -100
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/tests/test_settings.py +11 -12
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/tests/test_zone.py +2 -24
- vsslctrl-0.2.0/vsslctrl/__init__.py +7 -0
- vsslctrl-0.2.0/vsslctrl/api_alpha.py +1814 -0
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/api_base.py +124 -160
- vsslctrl-0.2.0/vsslctrl/api_bravo.py +645 -0
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/core.py +116 -124
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/data_structure.py +156 -62
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/decorators.py +46 -15
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/device.py +60 -11
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/discovery.py +42 -27
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/event_bus.py +19 -1
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/group.py +27 -55
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/io.py +5 -14
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/settings.py +153 -25
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/track.py +3 -45
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/transport.py +2 -2
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/utils.py +8 -17
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/zone.py +105 -117
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl.egg-info/PKG-INFO +35 -70
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl.egg-info/SOURCES.txt +2 -0
- vsslctrl-0.1.13.dev1/vsslctrl/__init__.py +0 -7
- vsslctrl-0.1.13.dev1/vsslctrl/api_alpha.py +0 -1265
- vsslctrl-0.1.13.dev1/vsslctrl/api_bravo.py +0 -427
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/LICENSE +0 -0
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/setup.cfg +0 -0
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl/exceptions.py +0 -0
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl.egg-info/dependency_links.txt +0 -0
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl.egg-info/requires.txt +0 -0
- {vsslctrl-0.1.13.dev1 → vsslctrl-0.2.0}/vsslctrl.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: vsslctrl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -18,28 +18,30 @@ Requires-Dist: pytest==8.1.1; extra == "dev"
|
|
|
18
18
|
Requires-Dist: pytest_asyncio==0.23.6; extra == "dev"
|
|
19
19
|
Provides-Extra: optional
|
|
20
20
|
Requires-Dist: zeroconf==0.132.2; extra == "optional"
|
|
21
|
+
Dynamic: license-file
|
|
21
22
|
|
|
22
23
|
# vsslctrl
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
Python package for controlling [VSSL's](https://www.vssl.com/) range of streaming amplifiers.
|
|
25
26
|
|
|
26
27
|
## Coverage
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
Test suite run on:
|
|
29
30
|
|
|
30
|
-
| Model | Software Version
|
|
31
|
-
| ------------|---------
|
|
32
|
-
| A.
|
|
31
|
+
| Model | Software Version
|
|
32
|
+
| ------------|---------
|
|
33
|
+
| `A.1x` | p15243.022.3703
|
|
34
|
+
| `A.3x` | p15305.016.3701
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) with basic functionality working on:
|
|
36
38
|
|
|
37
39
|
| Model | Software Version | User Reported |
|
|
38
40
|
| ------------|--------- | -------------
|
|
39
|
-
| A.1 | p15265.033.3703 | ✔️
|
|
40
|
-
| A.3
|
|
41
|
-
| A.3x | p15305.016.3701 |
|
|
42
|
-
| A.6x | p15305.017.3701 | ✔️
|
|
41
|
+
| `A.1` | p15265.033.3703 | ✔️
|
|
42
|
+
| `A.3` | p12013.141.3703 | ✔️
|
|
43
|
+
| `A.3x` | p15305.016.3701 |
|
|
44
|
+
| `A.6x` | p15305.017.3701 | ✔️
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
## Testers Needed
|
|
@@ -54,33 +56,28 @@ There should not be any *[VSSL Agent's](https://vssl.gitbook.io/vssl-rest-api/ge
|
|
|
54
56
|
|
|
55
57
|
**`vsslctrl` is not endorsed or affiliated with [VSSL](https://www.vssl.com/) in any manner.**
|
|
56
58
|
|
|
57
|
-
## TODOs
|
|
58
|
-
|
|
59
|
-
* Better test coverage
|
|
60
|
-
* A.1(x) testing - e.g output settings
|
|
61
|
-
* More controls - e.g IR Control
|
|
62
|
-
|
|
63
59
|
## Basic Usage
|
|
64
60
|
|
|
65
61
|
`vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
|
|
66
62
|
|
|
67
|
-
### A.1 Example
|
|
63
|
+
### `A.1(x)` Example
|
|
68
64
|
|
|
69
65
|
```python
|
|
70
66
|
import asyncio
|
|
71
|
-
from vsslctrl import Vssl
|
|
67
|
+
from vsslctrl import Vssl
|
|
72
68
|
|
|
73
69
|
async def main():
|
|
74
70
|
|
|
75
71
|
# Represents a physical VSSL amplifier
|
|
76
|
-
vssl = Vssl(
|
|
72
|
+
vssl = Vssl()
|
|
73
|
+
|
|
77
74
|
a1 = vssl.add_zone('192.168.1.10')
|
|
78
75
|
|
|
79
76
|
# Connect and initialise zone.
|
|
80
77
|
await vssl.initialise()
|
|
81
78
|
|
|
82
79
|
"""Control Examples"""
|
|
83
|
-
# Print device name
|
|
80
|
+
# Print zone/device name
|
|
84
81
|
print(a1.settings.name)
|
|
85
82
|
# Set volume to 25%
|
|
86
83
|
a1.volume = 25
|
|
@@ -95,21 +92,21 @@ async def main():
|
|
|
95
92
|
asyncio.run(main())
|
|
96
93
|
```
|
|
97
94
|
|
|
98
|
-
### A.
|
|
95
|
+
### `A.3(x)` Example
|
|
99
96
|
|
|
100
97
|
```python
|
|
101
98
|
import asyncio
|
|
102
|
-
from vsslctrl import Vssl
|
|
99
|
+
from vsslctrl import Vssl
|
|
103
100
|
|
|
104
101
|
async def main():
|
|
105
102
|
|
|
106
103
|
# Represents a physical VSSL amplifier
|
|
107
|
-
vssl = Vssl(
|
|
104
|
+
vssl = Vssl()
|
|
108
105
|
|
|
109
106
|
# Add each you wish to control
|
|
110
|
-
zone1 = vssl.add_zone('192.168.1.10'
|
|
111
|
-
zone2 = vssl.add_zone('192.168.1.11'
|
|
112
|
-
zone3 = vssl.add_zone('192.168.1.12'
|
|
107
|
+
zone1 = vssl.add_zone('192.168.1.10')
|
|
108
|
+
zone2 = vssl.add_zone('192.168.1.11')
|
|
109
|
+
zone3 = vssl.add_zone('192.168.1.12')
|
|
113
110
|
#... up to 6 zones for A.6(x)
|
|
114
111
|
|
|
115
112
|
# Connect and initialise zones.
|
|
@@ -205,37 +202,6 @@ print(zone1.settings.name)
|
|
|
205
202
|
|
|
206
203
|
# API Reference
|
|
207
204
|
|
|
208
|
-
# `DeviceModels`
|
|
209
|
-
|
|
210
|
-
A device model has to be passed to VSSL so it knows internally what features are supported by the device.
|
|
211
|
-
This might be removed in the future if we can differentiate different models from the API.
|
|
212
|
-
|
|
213
|
-
| Property | Description |
|
|
214
|
-
| ------------|--------- |
|
|
215
|
-
| `A1X` | A.1x |
|
|
216
|
-
| `A3X` | A.3x |
|
|
217
|
-
| `A6X` | A.6x |
|
|
218
|
-
| `A1` | A1 |
|
|
219
|
-
| `A3` | A3 |
|
|
220
|
-
| `A6` | A6 |
|
|
221
|
-
|
|
222
|
-
# `ZoneIDs`
|
|
223
|
-
|
|
224
|
-
A `ZoneIDs` must be passed to each `zone` you which to control and it must match the zone on the VSSL device.
|
|
225
|
-
|
|
226
|
-
If you are unsure of your `ZoneIDs` you could use the discovery helper to find out the correct mapping.
|
|
227
|
-
|
|
228
|
-
| Property | Description | A.1(x) | A.3(x) | A.6(x)
|
|
229
|
-
| ------------|--------- |-------- | -------- | -------- |
|
|
230
|
-
| `A1` | |✔️ | | |
|
|
231
|
-
| `ZONE_1` | Zone 1 | | ✔️| ✔️|
|
|
232
|
-
| `ZONE_2` | Zone 2 | | ✔️| ✔️|
|
|
233
|
-
| `ZONE_3` | Zone 3 | | ✔️| ✔️|
|
|
234
|
-
| `ZONE_4` | Zone 4 | | | ✔️|
|
|
235
|
-
| `ZONE_5` | Zone 5 | | | ✔️|
|
|
236
|
-
| `ZONE_6` | Zone 6 | | | ✔️|
|
|
237
|
-
|
|
238
|
-
|
|
239
205
|
# `Vssl`
|
|
240
206
|
|
|
241
207
|
| Property | Description | Type |
|
|
@@ -257,11 +223,13 @@ vssl.factory_reset()
|
|
|
257
223
|
|
|
258
224
|
## `Vssl.settings`
|
|
259
225
|
|
|
226
|
+
`A.1(x)` doesn't use `Vssl.settings.name` use `Zone.settings.name` instead.
|
|
227
|
+
|
|
260
228
|
| Property | Description | Type | Model: Default |
|
|
261
229
|
| ---------------------- | ----------- | ----------- | ----------- |
|
|
262
|
-
| `name` | Device name | `str` |
|
|
263
|
-
| `bus_1_name` | Name of Bus 1 | `str` | <ul><li
|
|
264
|
-
| `bus_2_name` | Name of Bus 2 | `str` | <ul><li
|
|
230
|
+
| `name` | Device name | `str` |
|
|
231
|
+
| `bus_1_name` | Name of Bus 1 | `str` | <ul><li>`A.1`: Optical Input</li><li>`A.3x`/`A.6x`: Not Used</li></ul>
|
|
232
|
+
| `bus_2_name` | Name of Bus 2 | `str` | <ul><li>`A.1`: Coax Input</li><li>`A.3x`/`A.6x`: Optical Input</li></ul>
|
|
265
233
|
| `bluetooth` | Bluetooth enabled / disabled | `bool` |
|
|
266
234
|
| `bluetooth_toggle()` | Toggle Bluetooth | `func` |
|
|
267
235
|
|
|
@@ -385,8 +353,8 @@ zone1.transport.state = ZoneTransport.States.PAUSE
|
|
|
385
353
|
| `priority` | Change input priority.<br/>Stream or local to have precedence | `int` | `InputRouter.Priorities`
|
|
386
354
|
|
|
387
355
|
|
|
388
|
-
### A.1(x) Source Routing Order
|
|
389
|
-
A.1 and A.1x don't support manually changing the input source. Instead a fixed source routing order is used:
|
|
356
|
+
### `A.1(x)` Source Routing Order
|
|
357
|
+
`A.1` and `A.1x` don't support manually changing the input source. Instead a fixed source routing order is used:
|
|
390
358
|
|
|
391
359
|
1. Optical Input
|
|
392
360
|
2. Coaxial Input
|
|
@@ -466,7 +434,7 @@ zone1.analog_output.is_fixed_volume = True
|
|
|
466
434
|
| `name` | Name | `str` |
|
|
467
435
|
| `disabled` | Disable the zone | `bool` || `False`
|
|
468
436
|
| `disabled_toggle()` | disable / enable | `func` |
|
|
469
|
-
| `mono` | Set output to mono or stereo | `int` | `ZoneSettings.StereoMono` | `
|
|
437
|
+
| `mono` | Set output to mono or stereo | `int` | `ZoneSettings.StereoMono` | `STEREO`
|
|
470
438
|
| `mono_toggle()` | Toggle mono or stereo | `func` |
|
|
471
439
|
|
|
472
440
|
```python
|
|
@@ -474,9 +442,9 @@ zone1.analog_output.is_fixed_volume = True
|
|
|
474
442
|
# Set name
|
|
475
443
|
zone1.settings.name = 'Living Room'
|
|
476
444
|
# Disable Zone
|
|
477
|
-
zone1.disabled = True
|
|
445
|
+
zone1.settings.disabled = True
|
|
478
446
|
# Toggle mono output
|
|
479
|
-
zone1.mono_toggle()
|
|
447
|
+
zone1.settings.mono_toggle()
|
|
480
448
|
```
|
|
481
449
|
|
|
482
450
|
## `Zone.settings.analog_input`
|
|
@@ -509,7 +477,7 @@ zone1.settings.analog_input.fixed_gain = 50
|
|
|
509
477
|
# Set default on volume to 50%
|
|
510
478
|
zone1.settings.volume.default_on = 50
|
|
511
479
|
# Set maximum volume for left channel to 75%
|
|
512
|
-
zone1.settings.volume.
|
|
480
|
+
zone1.settings.volume.max_left = 75
|
|
513
481
|
```
|
|
514
482
|
|
|
515
483
|
## `Zone.settings.eq`
|
|
@@ -561,12 +529,9 @@ Motivation for this project was to integrate VSSLs amplifiers into [Home Assista
|
|
|
561
529
|
|
|
562
530
|
## Known Issues & Limitiations
|
|
563
531
|
|
|
564
|
-
* Not tested on A.1x or original A series range of amplifiers (testers welcome)
|
|
565
532
|
* VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
|
|
566
533
|
* Not all sources set the volume to 0 when the zone is muted
|
|
567
534
|
* Airplay `Zone.track.progress` is not available.
|
|
568
|
-
* Cant stop a URL playback, feedback is worng at least
|
|
569
535
|
* 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
|
|
570
536
|
* `stop()` is intended to disconnect the client and pause the stream. Doesn’t always function this way, depending on stream source
|
|
571
|
-
* Occasionally a zones might stop responding to certain commands, issuing the `reboot` command generally corrects
|
|
572
537
|
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
# vsslctrl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Python package for controlling [VSSL's](https://www.vssl.com/) range of streaming amplifiers.
|
|
4
4
|
|
|
5
5
|
## Coverage
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Test suite run on:
|
|
8
8
|
|
|
9
|
-
| Model | Software Version
|
|
10
|
-
| ------------|---------
|
|
11
|
-
| A.
|
|
9
|
+
| Model | Software Version
|
|
10
|
+
| ------------|---------
|
|
11
|
+
| `A.1x` | p15243.022.3703
|
|
12
|
+
| `A.3x` | p15305.016.3701
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) with basic functionality working on:
|
|
15
16
|
|
|
16
17
|
| Model | Software Version | User Reported |
|
|
17
18
|
| ------------|--------- | -------------
|
|
18
|
-
| A.1 | p15265.033.3703 | ✔️
|
|
19
|
-
| A.3
|
|
20
|
-
| A.3x | p15305.016.3701 |
|
|
21
|
-
| A.6x | p15305.017.3701 | ✔️
|
|
19
|
+
| `A.1` | p15265.033.3703 | ✔️
|
|
20
|
+
| `A.3` | p12013.141.3703 | ✔️
|
|
21
|
+
| `A.3x` | p15305.016.3701 |
|
|
22
|
+
| `A.6x` | p15305.017.3701 | ✔️
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
## Testers Needed
|
|
@@ -33,33 +34,28 @@ There should not be any *[VSSL Agent's](https://vssl.gitbook.io/vssl-rest-api/ge
|
|
|
33
34
|
|
|
34
35
|
**`vsslctrl` is not endorsed or affiliated with [VSSL](https://www.vssl.com/) in any manner.**
|
|
35
36
|
|
|
36
|
-
## TODOs
|
|
37
|
-
|
|
38
|
-
* Better test coverage
|
|
39
|
-
* A.1(x) testing - e.g output settings
|
|
40
|
-
* More controls - e.g IR Control
|
|
41
|
-
|
|
42
37
|
## Basic Usage
|
|
43
38
|
|
|
44
39
|
`vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
|
|
45
40
|
|
|
46
|
-
### A.1 Example
|
|
41
|
+
### `A.1(x)` Example
|
|
47
42
|
|
|
48
43
|
```python
|
|
49
44
|
import asyncio
|
|
50
|
-
from vsslctrl import Vssl
|
|
45
|
+
from vsslctrl import Vssl
|
|
51
46
|
|
|
52
47
|
async def main():
|
|
53
48
|
|
|
54
49
|
# Represents a physical VSSL amplifier
|
|
55
|
-
vssl = Vssl(
|
|
50
|
+
vssl = Vssl()
|
|
51
|
+
|
|
56
52
|
a1 = vssl.add_zone('192.168.1.10')
|
|
57
53
|
|
|
58
54
|
# Connect and initialise zone.
|
|
59
55
|
await vssl.initialise()
|
|
60
56
|
|
|
61
57
|
"""Control Examples"""
|
|
62
|
-
# Print device name
|
|
58
|
+
# Print zone/device name
|
|
63
59
|
print(a1.settings.name)
|
|
64
60
|
# Set volume to 25%
|
|
65
61
|
a1.volume = 25
|
|
@@ -74,21 +70,21 @@ async def main():
|
|
|
74
70
|
asyncio.run(main())
|
|
75
71
|
```
|
|
76
72
|
|
|
77
|
-
### A.
|
|
73
|
+
### `A.3(x)` Example
|
|
78
74
|
|
|
79
75
|
```python
|
|
80
76
|
import asyncio
|
|
81
|
-
from vsslctrl import Vssl
|
|
77
|
+
from vsslctrl import Vssl
|
|
82
78
|
|
|
83
79
|
async def main():
|
|
84
80
|
|
|
85
81
|
# Represents a physical VSSL amplifier
|
|
86
|
-
vssl = Vssl(
|
|
82
|
+
vssl = Vssl()
|
|
87
83
|
|
|
88
84
|
# Add each you wish to control
|
|
89
|
-
zone1 = vssl.add_zone('192.168.1.10'
|
|
90
|
-
zone2 = vssl.add_zone('192.168.1.11'
|
|
91
|
-
zone3 = vssl.add_zone('192.168.1.12'
|
|
85
|
+
zone1 = vssl.add_zone('192.168.1.10')
|
|
86
|
+
zone2 = vssl.add_zone('192.168.1.11')
|
|
87
|
+
zone3 = vssl.add_zone('192.168.1.12')
|
|
92
88
|
#... up to 6 zones for A.6(x)
|
|
93
89
|
|
|
94
90
|
# Connect and initialise zones.
|
|
@@ -184,37 +180,6 @@ print(zone1.settings.name)
|
|
|
184
180
|
|
|
185
181
|
# API Reference
|
|
186
182
|
|
|
187
|
-
# `DeviceModels`
|
|
188
|
-
|
|
189
|
-
A device model has to be passed to VSSL so it knows internally what features are supported by the device.
|
|
190
|
-
This might be removed in the future if we can differentiate different models from the API.
|
|
191
|
-
|
|
192
|
-
| Property | Description |
|
|
193
|
-
| ------------|--------- |
|
|
194
|
-
| `A1X` | A.1x |
|
|
195
|
-
| `A3X` | A.3x |
|
|
196
|
-
| `A6X` | A.6x |
|
|
197
|
-
| `A1` | A1 |
|
|
198
|
-
| `A3` | A3 |
|
|
199
|
-
| `A6` | A6 |
|
|
200
|
-
|
|
201
|
-
# `ZoneIDs`
|
|
202
|
-
|
|
203
|
-
A `ZoneIDs` must be passed to each `zone` you which to control and it must match the zone on the VSSL device.
|
|
204
|
-
|
|
205
|
-
If you are unsure of your `ZoneIDs` you could use the discovery helper to find out the correct mapping.
|
|
206
|
-
|
|
207
|
-
| Property | Description | A.1(x) | A.3(x) | A.6(x)
|
|
208
|
-
| ------------|--------- |-------- | -------- | -------- |
|
|
209
|
-
| `A1` | |✔️ | | |
|
|
210
|
-
| `ZONE_1` | Zone 1 | | ✔️| ✔️|
|
|
211
|
-
| `ZONE_2` | Zone 2 | | ✔️| ✔️|
|
|
212
|
-
| `ZONE_3` | Zone 3 | | ✔️| ✔️|
|
|
213
|
-
| `ZONE_4` | Zone 4 | | | ✔️|
|
|
214
|
-
| `ZONE_5` | Zone 5 | | | ✔️|
|
|
215
|
-
| `ZONE_6` | Zone 6 | | | ✔️|
|
|
216
|
-
|
|
217
|
-
|
|
218
183
|
# `Vssl`
|
|
219
184
|
|
|
220
185
|
| Property | Description | Type |
|
|
@@ -236,11 +201,13 @@ vssl.factory_reset()
|
|
|
236
201
|
|
|
237
202
|
## `Vssl.settings`
|
|
238
203
|
|
|
204
|
+
`A.1(x)` doesn't use `Vssl.settings.name` use `Zone.settings.name` instead.
|
|
205
|
+
|
|
239
206
|
| Property | Description | Type | Model: Default |
|
|
240
207
|
| ---------------------- | ----------- | ----------- | ----------- |
|
|
241
|
-
| `name` | Device name | `str` |
|
|
242
|
-
| `bus_1_name` | Name of Bus 1 | `str` | <ul><li
|
|
243
|
-
| `bus_2_name` | Name of Bus 2 | `str` | <ul><li
|
|
208
|
+
| `name` | Device name | `str` |
|
|
209
|
+
| `bus_1_name` | Name of Bus 1 | `str` | <ul><li>`A.1`: Optical Input</li><li>`A.3x`/`A.6x`: Not Used</li></ul>
|
|
210
|
+
| `bus_2_name` | Name of Bus 2 | `str` | <ul><li>`A.1`: Coax Input</li><li>`A.3x`/`A.6x`: Optical Input</li></ul>
|
|
244
211
|
| `bluetooth` | Bluetooth enabled / disabled | `bool` |
|
|
245
212
|
| `bluetooth_toggle()` | Toggle Bluetooth | `func` |
|
|
246
213
|
|
|
@@ -364,8 +331,8 @@ zone1.transport.state = ZoneTransport.States.PAUSE
|
|
|
364
331
|
| `priority` | Change input priority.<br/>Stream or local to have precedence | `int` | `InputRouter.Priorities`
|
|
365
332
|
|
|
366
333
|
|
|
367
|
-
### A.1(x) Source Routing Order
|
|
368
|
-
A.1 and A.1x don't support manually changing the input source. Instead a fixed source routing order is used:
|
|
334
|
+
### `A.1(x)` Source Routing Order
|
|
335
|
+
`A.1` and `A.1x` don't support manually changing the input source. Instead a fixed source routing order is used:
|
|
369
336
|
|
|
370
337
|
1. Optical Input
|
|
371
338
|
2. Coaxial Input
|
|
@@ -445,7 +412,7 @@ zone1.analog_output.is_fixed_volume = True
|
|
|
445
412
|
| `name` | Name | `str` |
|
|
446
413
|
| `disabled` | Disable the zone | `bool` || `False`
|
|
447
414
|
| `disabled_toggle()` | disable / enable | `func` |
|
|
448
|
-
| `mono` | Set output to mono or stereo | `int` | `ZoneSettings.StereoMono` | `
|
|
415
|
+
| `mono` | Set output to mono or stereo | `int` | `ZoneSettings.StereoMono` | `STEREO`
|
|
449
416
|
| `mono_toggle()` | Toggle mono or stereo | `func` |
|
|
450
417
|
|
|
451
418
|
```python
|
|
@@ -453,9 +420,9 @@ zone1.analog_output.is_fixed_volume = True
|
|
|
453
420
|
# Set name
|
|
454
421
|
zone1.settings.name = 'Living Room'
|
|
455
422
|
# Disable Zone
|
|
456
|
-
zone1.disabled = True
|
|
423
|
+
zone1.settings.disabled = True
|
|
457
424
|
# Toggle mono output
|
|
458
|
-
zone1.mono_toggle()
|
|
425
|
+
zone1.settings.mono_toggle()
|
|
459
426
|
```
|
|
460
427
|
|
|
461
428
|
## `Zone.settings.analog_input`
|
|
@@ -488,7 +455,7 @@ zone1.settings.analog_input.fixed_gain = 50
|
|
|
488
455
|
# Set default on volume to 50%
|
|
489
456
|
zone1.settings.volume.default_on = 50
|
|
490
457
|
# Set maximum volume for left channel to 75%
|
|
491
|
-
zone1.settings.volume.
|
|
458
|
+
zone1.settings.volume.max_left = 75
|
|
492
459
|
```
|
|
493
460
|
|
|
494
461
|
## `Zone.settings.eq`
|
|
@@ -540,12 +507,9 @@ Motivation for this project was to integrate VSSLs amplifiers into [Home Assista
|
|
|
540
507
|
|
|
541
508
|
## Known Issues & Limitiations
|
|
542
509
|
|
|
543
|
-
* Not tested on A.1x or original A series range of amplifiers (testers welcome)
|
|
544
510
|
* VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
|
|
545
511
|
* Not all sources set the volume to 0 when the zone is muted
|
|
546
512
|
* Airplay `Zone.track.progress` is not available.
|
|
547
|
-
* Cant stop a URL playback, feedback is worng at least
|
|
548
513
|
* 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
|
|
549
514
|
* `stop()` is intended to disconnect the client and pause the stream. Doesn’t always function this way, depending on stream source
|
|
550
|
-
* Occasionally a zones might stop responding to certain commands, issuing the `reboot` command generally corrects
|
|
551
515
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vsslctrl"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Package for controlling VSSL's range of streaming amplifiers"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="vsslctrl", email="vsslcontrolled@proton.me" },
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import pytest
|
|
3
|
+
import pytest_asyncio
|
|
4
|
+
|
|
5
|
+
from vsslctrl import Vssl
|
|
6
|
+
from vsslctrl.api_alpha import APIAlpha
|
|
7
|
+
from vsslctrl.api_bravo import APIBravo
|
|
8
|
+
|
|
9
|
+
FUTURE_TIMEOUT = 5
|
|
10
|
+
|
|
11
|
+
# Mark all tests in this module with the pytest custom "integration" marker so
|
|
12
|
+
# they can be selected or deselected as a whole, eg:
|
|
13
|
+
# py.test -m "integration"
|
|
14
|
+
# or
|
|
15
|
+
# py.test -m "no integration"
|
|
16
|
+
pytestmark = pytest.mark.integration
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest_asyncio.fixture(scope="session")
|
|
20
|
+
async def zone(request):
|
|
21
|
+
ip = request.config.option.ip
|
|
22
|
+
if ip is None:
|
|
23
|
+
pytest.fail("No ip address specified. Use the --ip option.")
|
|
24
|
+
|
|
25
|
+
vssl = Vssl()
|
|
26
|
+
|
|
27
|
+
zone = vssl.add_zone(ip)
|
|
28
|
+
|
|
29
|
+
await vssl.initialise()
|
|
30
|
+
|
|
31
|
+
if not zone.initialised:
|
|
32
|
+
pytest.fail(f"Zone not initialised, dunno!")
|
|
33
|
+
|
|
34
|
+
# Yield the device to the test function
|
|
35
|
+
yield zone
|
|
36
|
+
|
|
37
|
+
# Tear down. Restore state
|
|
38
|
+
await vssl.disconnect()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest_asyncio.fixture(scope="session")
|
|
42
|
+
async def eb(zone):
|
|
43
|
+
return zone.vssl.event_bus
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest_asyncio.fixture(scope="session")
|
|
47
|
+
async def vssl(zone):
|
|
48
|
+
return zone.vssl
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestAPI:
|
|
52
|
+
@pytest.mark.asyncio
|
|
53
|
+
async def test_api_has_events(self):
|
|
54
|
+
required_events = [
|
|
55
|
+
"CONNECTING",
|
|
56
|
+
"CONNECTED",
|
|
57
|
+
"DISCONNECTING",
|
|
58
|
+
"DISCONNECTED",
|
|
59
|
+
"RECONNECTING",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for event in required_events:
|
|
63
|
+
assert hasattr(APIAlpha.Events, event)
|
|
64
|
+
assert hasattr(APIBravo.Events, event)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import pytest
|
|
3
|
+
import pytest_asyncio
|
|
4
|
+
|
|
5
|
+
from vsslctrl import VSSL_VERSION
|
|
6
|
+
from vsslctrl.data_structure import ModelIDs
|
|
7
|
+
from vsslctrl.device import Models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestDevice:
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
async def test_device_models(self):
|
|
13
|
+
model = Models.A1X.value
|
|
14
|
+
assert model.model_id == ModelIDs.A1X
|
|
15
|
+
|
|
16
|
+
model = Models.get_by_name("A.1x")
|
|
17
|
+
assert model.model_id == ModelIDs.A1X
|
|
18
|
+
assert model.model_id != ModelIDs.A1
|
|
19
|
+
assert model.name == "A.1x"
|
|
20
|
+
assert len(model.zones) == 1
|
|
21
|
+
|
|
22
|
+
model = Models.get_by_name("A1")
|
|
23
|
+
assert model.model_id != ModelIDs.A1X
|
|
24
|
+
assert model.model_id == ModelIDs.A1
|
|
25
|
+
|
|
26
|
+
model = Models.get_by_id(ModelIDs.A1X)
|
|
27
|
+
assert model.model_id == ModelIDs.A1X
|
|
28
|
+
|
|
29
|
+
model = Models.get_by_id("11")
|
|
30
|
+
assert model.model_id == ModelIDs.A1X
|
|
31
|
+
|
|
32
|
+
model = Models.get_by_name("random string")
|
|
33
|
+
assert model == None
|
|
34
|
+
|
|
35
|
+
model = Models.get_by_id(10)
|
|
36
|
+
assert model == None
|
|
37
|
+
|
|
38
|
+
model = Models.get_by_id("random string")
|
|
39
|
+
assert model == None
|
|
40
|
+
|
|
41
|
+
model = Models.get_by_id(ModelIDs.A3X)
|
|
42
|
+
assert model.model_id == ModelIDs.A3X
|
|
43
|
+
assert len(model.zones) == 3
|
|
44
|
+
|
|
45
|
+
model = Models.get_by_id(ModelIDs.A3)
|
|
46
|
+
assert model.model_id == ModelIDs.A3
|
|
47
|
+
assert len(model.zones) == 3
|
|
48
|
+
|
|
49
|
+
model = Models.get_by_id(ModelIDs.A6X)
|
|
50
|
+
assert model.model_id == ModelIDs.A6X
|
|
51
|
+
assert len(model.zones) == 6
|
|
52
|
+
|
|
53
|
+
model = Models.get_by_id(ModelIDs.A6)
|
|
54
|
+
assert model.model_id == ModelIDs.A6
|
|
55
|
+
assert len(model.zones) == 6
|
|
56
|
+
|
|
57
|
+
# Test Find
|
|
58
|
+
model = Models.find("A.1x")
|
|
59
|
+
assert model.model_id == ModelIDs.A1X
|
|
60
|
+
assert model.model_id != ModelIDs.A1
|
|
61
|
+
assert model.name == "A.1x"
|
|
62
|
+
assert len(model.zones) == 1
|
|
63
|
+
assert Models.is_valid(model)
|
|
64
|
+
|
|
65
|
+
model = Models.find("vssl A.6x")
|
|
66
|
+
assert model.model_id == ModelIDs.A6X
|
|
67
|
+
assert model.model_id != ModelIDs.A6
|
|
68
|
+
assert model.name == "A.6x"
|
|
69
|
+
assert len(model.zones) == 6
|
|
70
|
+
|
|
71
|
+
model = Models.find("vssl A6")
|
|
72
|
+
assert model.model_id == ModelIDs.A6
|
|
73
|
+
assert model.model_id != ModelIDs.A6X
|
|
74
|
+
assert model.name == "A.6"
|
|
75
|
+
assert len(model.zones) == 6
|
|
76
|
+
|
|
77
|
+
model = Models.find("A 3")
|
|
78
|
+
assert model.model_id == ModelIDs.A3
|
|
79
|
+
assert model.model_id != ModelIDs.A3X
|
|
80
|
+
assert model.name == "A.3"
|
|
81
|
+
assert len(model.zones) == 3
|
|
82
|
+
|
|
83
|
+
model = Models.find("VSSL A.3x")
|
|
84
|
+
assert model.model_id == ModelIDs.A3X
|
|
85
|
+
assert model.model_id != ModelIDs.A3
|
|
86
|
+
assert model.name == "A.3x"
|
|
87
|
+
assert len(model.zones) == 3
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import pytest
|
|
3
3
|
import pytest_asyncio
|
|
4
|
-
from vsslctrl.event_bus import
|
|
4
|
+
from vsslctrl.event_bus import event_bus as _event_bus
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class TestEventBus:
|
|
8
8
|
@pytest_asyncio.fixture(autouse=True)
|
|
9
9
|
async def event_bus(self):
|
|
10
|
-
|
|
11
|
-
yield
|
|
12
|
-
|
|
10
|
+
event_bus = _event_bus()
|
|
11
|
+
yield event_bus
|
|
12
|
+
event_bus.stop() # Ensure the event bus is stopped after each test
|
|
13
13
|
|
|
14
14
|
@pytest.mark.asyncio
|
|
15
15
|
async def test_event_bus(self, event_bus):
|
|
@@ -19,6 +19,7 @@ class TestEventBus:
|
|
|
19
19
|
async def publish_with_entity(self, entity=None):
|
|
20
20
|
# Define a callback function to be subscribed to the event
|
|
21
21
|
async def callback(data, *args, **kwargs):
|
|
22
|
+
print("hello")
|
|
22
23
|
callback.called = True
|
|
23
24
|
callback.data = data
|
|
24
25
|
|