vsslctrl 0.1.9.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.
Files changed (32) hide show
  1. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/PKG-INFO +100 -37
  2. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/README.md +99 -36
  3. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/pyproject.toml +1 -1
  4. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/__init__.py +3 -2
  5. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/api_alpha.py +57 -9
  6. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/core.py +14 -0
  7. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/data_structure.py +74 -17
  8. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/device.py +8 -7
  9. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/group.py +30 -4
  10. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/io.py +13 -7
  11. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/settings.py +13 -8
  12. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl.egg-info/PKG-INFO +100 -37
  13. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/LICENSE +0 -0
  14. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/setup.cfg +0 -0
  15. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/tests/test_event_bus.py +0 -0
  16. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/tests/test_integration.py +0 -0
  17. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/tests/test_settings.py +0 -0
  18. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/tests/test_zone.py +0 -0
  19. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/api_base.py +0 -0
  20. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/api_bravo.py +0 -0
  21. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/decorators.py +0 -0
  22. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/discovery.py +0 -0
  23. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/event_bus.py +0 -0
  24. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/exceptions.py +0 -0
  25. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/track.py +0 -0
  26. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/transport.py +0 -0
  27. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/utils.py +0 -0
  28. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl/zone.py +0 -0
  29. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl.egg-info/SOURCES.txt +0 -0
  30. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl.egg-info/dependency_links.txt +0 -0
  31. {vsslctrl-0.1.9.dev1 → vsslctrl-0.1.10.dev1}/vsslctrl.egg-info/requires.txt +0 -0
  32. {vsslctrl-0.1.9.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.9.dev1
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
- - Test suite run on a VSSL **A.3x** software version **p15305.016.3701**
34
- - The Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) is reported working on a **A.6x** software version **p15305.017.3701**
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
- * **A.1(x)** specific control e.g sub crossover, bluetooth
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
- # Represents a physical VSSL amplifier
56
- vssl = Vssl()
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
- """Optional to init Vssl with a device model
79
+ asyncio.run(main())
80
+ ```
59
81
 
60
- vssl = Vssl(DeviceModels.A3X)
82
+ ### A.3x Example
61
83
 
62
- If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
63
- """
84
+ ```python
85
+ import asyncio
86
+ from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
64
87
 
65
- # Add each you wish to control
66
- zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
67
- zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
68
- zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
69
- #... up to 6 zones
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
- # Connect and initiate zones.
72
- await vssl.initialise()
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
- """Control Examples"""
75
- # Print zone1 name
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
- # Shutdown and disconnect all zones
87
- await vssl.shutdown()
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 confimation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events.
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 avaiable.
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
- Working on A.3x but offically unsupported in x series amplifiers.
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
- Motovation for this project was to intergrate VSSLs amplifiers into [Home Assistant](https://www.home-assistant.io/) and have control over different subnets (not mDNS dependant)
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 avaiable.
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. Doesnt always function this way, depending on stream source
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 and subwoofer control
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
- - Test suite run on a VSSL **A.3x** software version **p15305.016.3701**
13
- - The Home Assistant [integration](https://github.com/vsslctrl/integration.home-assistant) is reported working on a **A.6x** software version **p15305.017.3701**
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
- * **A.1(x)** specific control e.g sub crossover, bluetooth
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
- # Represents a physical VSSL amplifier
35
- vssl = Vssl()
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
- """Optional to init Vssl with a device model
58
+ asyncio.run(main())
59
+ ```
38
60
 
39
- vssl = Vssl(DeviceModels.A3X)
61
+ ### A.3x Example
40
62
 
41
- If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
42
- """
63
+ ```python
64
+ import asyncio
65
+ from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
43
66
 
44
- # Add each you wish to control
45
- zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
46
- zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
47
- zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
48
- #... up to 6 zones
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
- # Connect and initiate zones.
51
- await vssl.initialise()
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
- """Control Examples"""
54
- # Print zone1 name
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
- # Shutdown and disconnect all zones
66
- await vssl.shutdown()
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 confimation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events.
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 avaiable.
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
- Working on A.3x but offically unsupported in x series amplifiers.
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
- Motovation for this project was to intergrate VSSLs amplifiers into [Home Assistant](https://www.home-assistant.io/) and have control over different subnets (not mDNS dependant)
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 avaiable.
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. Doesnt always function this way, depending on stream source
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 and subwoofer control
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.9.dev1"
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" },
@@ -1,6 +1,7 @@
1
+ VSSL_VERSION = "0.1.10.dev1"
2
+ VSSL_NAME = "VSSL"
3
+
1
4
  from .core import Vssl
2
5
  from .zone import Zone
3
6
  from .data_structure import ZoneIDs
4
7
  from .device import Models as DeviceModels
5
-
6
- VSSL_NAME = "VSSL"
@@ -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(f"Requesting to reboot device")
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
- # Not supported by X series?
666
- if ZoneStatusExtKeys.PARTY_MODE in metadata:
667
- pass
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
- # 0B [11]
933
- # Party Mode not supported on X series (I think. TODO)
966
+ # 0C [12]
967
+ # Party Mode
934
968
  #
935
- def response_action_0B(self, hexl: list, response: bytes):
936
- pass
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 requency:{freq} value: {value}")
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
  #