vsslctrl 0.1.0.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 (31) hide show
  1. vsslctrl-0.1.0.dev1/LICENSE +21 -0
  2. vsslctrl-0.1.0.dev1/PKG-INFO +385 -0
  3. vsslctrl-0.1.0.dev1/README.md +364 -0
  4. vsslctrl-0.1.0.dev1/pyproject.toml +27 -0
  5. vsslctrl-0.1.0.dev1/setup.cfg +4 -0
  6. vsslctrl-0.1.0.dev1/tests/test_event_bus.py +70 -0
  7. vsslctrl-0.1.0.dev1/tests/test_integration.py +607 -0
  8. vsslctrl-0.1.0.dev1/tests/test_settings.py +114 -0
  9. vsslctrl-0.1.0.dev1/tests/test_zone.py +58 -0
  10. vsslctrl-0.1.0.dev1/vsslctrl/__init__.py +4 -0
  11. vsslctrl-0.1.0.dev1/vsslctrl/api_alpha.py +1040 -0
  12. vsslctrl-0.1.0.dev1/vsslctrl/api_base.py +321 -0
  13. vsslctrl-0.1.0.dev1/vsslctrl/api_bravo.py +419 -0
  14. vsslctrl-0.1.0.dev1/vsslctrl/core.py +322 -0
  15. vsslctrl-0.1.0.dev1/vsslctrl/data_structure.py +242 -0
  16. vsslctrl-0.1.0.dev1/vsslctrl/decorators.py +61 -0
  17. vsslctrl-0.1.0.dev1/vsslctrl/discovery.py +193 -0
  18. vsslctrl-0.1.0.dev1/vsslctrl/event_bus.py +159 -0
  19. vsslctrl-0.1.0.dev1/vsslctrl/exceptions.py +21 -0
  20. vsslctrl-0.1.0.dev1/vsslctrl/group.py +187 -0
  21. vsslctrl-0.1.0.dev1/vsslctrl/io.py +229 -0
  22. vsslctrl-0.1.0.dev1/vsslctrl/settings.py +655 -0
  23. vsslctrl-0.1.0.dev1/vsslctrl/track.py +337 -0
  24. vsslctrl-0.1.0.dev1/vsslctrl/transport.py +242 -0
  25. vsslctrl-0.1.0.dev1/vsslctrl/utils.py +75 -0
  26. vsslctrl-0.1.0.dev1/vsslctrl/zone.py +452 -0
  27. vsslctrl-0.1.0.dev1/vsslctrl.egg-info/PKG-INFO +385 -0
  28. vsslctrl-0.1.0.dev1/vsslctrl.egg-info/SOURCES.txt +29 -0
  29. vsslctrl-0.1.0.dev1/vsslctrl.egg-info/dependency_links.txt +1 -0
  30. vsslctrl-0.1.0.dev1/vsslctrl.egg-info/requires.txt +7 -0
  31. vsslctrl-0.1.0.dev1/vsslctrl.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 VSSLCTRL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,385 @@
1
+ Metadata-Version: 2.1
2
+ Name: vsslctrl
3
+ Version: 0.1.0.dev1
4
+ Summary: Package for controlling VSSL amplifiers
5
+ Author-email: vsslctrl <vsslcontrolled@proton.me>
6
+ Classifier: Development Status :: 4 - Beta
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Home Automation
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest==8.1.1; extra == "dev"
18
+ Requires-Dist: pytest_asyncio==0.23.6; extra == "dev"
19
+ Provides-Extra: optional
20
+ Requires-Dist: zeroconf==0.132.2; extra == "optional"
21
+
22
+ # vsslctrl
23
+ Package for controlling [VSSL](https://www.vssl.com/) range of streaming amplifiers.
24
+
25
+ **`vsslctrl` is not endorsed or affiliated with [VSSL](https://www.vssl.com/) in any manner.**
26
+
27
+ Motovation for this project was to intergrate VSSLs amplifiers into [Home Assistant](https://www.home-assistant.io/) and not soley rely on mDNS for control (as per the offical VSSL app)
28
+
29
+ I am looking for testers with any VSSL amplifier models, please get in touch if you interested in helping.
30
+
31
+ Only tested on VSSL **A.3x** software version **p15305.016.3701**.
32
+
33
+ ## TODOs
34
+
35
+ * Test on other models (hardware needed)
36
+ * Home Assistant integration (in progress)
37
+ * Function scoping to supported feature / models
38
+ * Better test coverage
39
+
40
+ Basic Usage
41
+ -----------
42
+
43
+ `vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
44
+
45
+ ```python
46
+ import asyncio
47
+ from vsslctrl import Vssl, Zone
48
+
49
+ async def main():
50
+
51
+ # Represents a physical VSSL amplifier
52
+ vssl = Vssl()
53
+
54
+ # Add each you wish to control
55
+ zone1 = vssl.add_zone(Zone.IDs.ZONE_1, '192.168.1.10')
56
+ zone2 = vssl.add_zone(Zone.IDs.ZONE_2, '192.168.1.11')
57
+ zone3 = vssl.add_zone(Zone.IDs.ZONE_3, '192.168.1.12')
58
+ #... up to 6 zones
59
+
60
+ # Connect and initiate zones.
61
+ await vssl.initialise()
62
+
63
+ """Control Examples"""
64
+ # Print zone1 name
65
+ print(zone1.settings.name)
66
+ # Set zone2 volume to 25%
67
+ zone2.volume = 25
68
+ # Pause zone3
69
+ zone3.pause()
70
+ # or zone3.transport.pause()
71
+ # Print zone1 track name
72
+ print(zone1.track.name)
73
+
74
+
75
+ # Shutdown and disconnect all zones
76
+ await vssl.shutdown()
77
+
78
+
79
+ asyncio.run(main())
80
+ ```
81
+
82
+ API
83
+ -----------
84
+
85
+ Most functionality is achived via `getters` and `setters` of the two main classes `Vssl`, `Zone`.
86
+
87
+ The classes will update the physical VSSL device when setting a property and once feedback has been received, the classes internal state will be updated. For example:
88
+
89
+ ```python
90
+ # Setting the zones name
91
+ zone1.settings.name = 'Living Room'
92
+ >>> None
93
+
94
+ # Printing zone name
95
+ zone_name = zone1.settings.name
96
+ print(zone_name)
97
+ >>> 'Living Room'
98
+ ```
99
+
100
+ **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.
101
+
102
+
103
+ # `Vssl`
104
+
105
+ | Property | Description | Type |
106
+ | ---------------------- | ----------- | ----------- |
107
+ | `sw_version` | Software version | `str` readonly
108
+ | `serial` | Serial number | `str` readonly
109
+ | `model_zone_qty` | Number of zones the device has | `int` readonly
110
+ | `reboot()` | Reboot all zones | `func` |
111
+
112
+ ```python
113
+ """Example"""
114
+ # Reboot all zones
115
+ vssl.reboot()
116
+ ```
117
+
118
+ ## `Vssl.settings`
119
+
120
+ | Property | Description | Type |
121
+ | ---------------------- | ----------- | ----------- |
122
+ | `name` | Device name | `str`
123
+ | `optical_input_name` | Name of the optical input | `str`
124
+
125
+ ```python
126
+ """Example"""
127
+ # Setting device name
128
+ vssl.settings.name = 'My House'
129
+ # Setting optical input name
130
+ vssl.settings.optical_input_name = 'Optical Input 1'
131
+ ```
132
+
133
+ ## `Vssl.settings.power`
134
+
135
+ | Property | Description | Type | Values |
136
+ | ---------------------- | ----------- | ----------- |----------- |
137
+ | `state` | Power state | `int` readonly | `VsslPowerSettings.States`
138
+ | `adaptive` | Power adaptive | `bool`
139
+
140
+ ```python
141
+ """Example"""
142
+ # Setting power adaptive
143
+ vssl.settings.power.adaptive = True
144
+ ```
145
+
146
+
147
+ # `Zone`
148
+
149
+ | Property | Description | Type | Values |
150
+ | ---------------------- | ----------- | ----------- |----------- |
151
+ | `id` | Zone number / ID | `int` readonly | `Zone.IDs`
152
+ | `host` | IP address | `str` readonly
153
+ | `volume` | Volume | `int` | `0...100`
154
+ | `volume_raise([step=1])` | Raise volume by `step` | `func` | step: `int` `1...100`
155
+ | `volume_lower([step=1])` | Lower volume by `step` | `func` | step: `int` `1...100`
156
+ | `mute` | Volume muted | `bool` |
157
+ | `mute_toggle()` | Mute / Unmute | `func` |
158
+ | `play()` | Play | `func` |
159
+ | `stop()` | Stop | `func` |
160
+ | `pause()` | Pause | `func` |
161
+ | `next()` | Next track | `func` |
162
+ | `prev()` | Begining of track or previous track | `func` |
163
+ | `reboot()` | Reboot zone | `func` |
164
+ | `play_url([url], [all_zones])` | Play a URL | `func` | url: `str`, all_zones: `bool`
165
+
166
+
167
+ ```python
168
+ """Examples"""
169
+ # Set volume to 50%
170
+ zone1.volume = 50
171
+ # Raise volume by 5%
172
+ zone1.volume_raise(5)
173
+ # Mute
174
+ zone1.mute = True
175
+ # Toggle mute
176
+ zone1.mute_toggle()
177
+ # Pause transport
178
+ zone1.pause()
179
+ # Next track
180
+ zone1.next()
181
+ # Play a URL on this zone1
182
+ zone1.play_url('http://soundbible.com/grab.php?id=2217&type=mp3')
183
+ # Play a URL on all zones
184
+ zone1.play_url('http://soundbible.com/grab.php?id=2217&type=mp3', True)
185
+ ```
186
+
187
+ ## `Zone.transport`
188
+
189
+ A VSSL amplifier can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
190
+
191
+ | Property | Description | Type | Values |
192
+ | ---------------------- | ----------- | ----------- |----------- |
193
+ | `state` | Transport state. i.e Play, Stop, Pause | `int` | `ZoneTransport.States`
194
+ | `play()` | Play | `func` |
195
+ | `stop()` | Stop | `func` |
196
+ | `pause()` | Pause | `func` |
197
+ | `next()` | Next track | `func` |
198
+ | `prev()` | Begining of track or previous track | `func` |
199
+ | `is_playing` | Is the zone playing | `bool` readonly
200
+ | `is_stopped` | Is the zone stopped | `bool` readonly
201
+ | `is_pasued` | Is the zone pasued | `bool` readonly
202
+ | `is_repeat` | Repeat state. i.e all, one, off | `int` readonly | `ZoneTransport.Repeat`
203
+ | `is_shuffle` | Is shuffle enabled | `bool` readonly
204
+ | `has_next` | Is the next button enabled | `bool` readonly
205
+ | `has_prev` | Is the prev button enabled | `bool` readonly
206
+
207
+ ```python
208
+ """Example"""
209
+ # Pause the stream
210
+ zone1.transport.pause()
211
+ # or
212
+ zone1.transport.state = ZoneTransport.States.PAUSE
213
+ ```
214
+
215
+ ## `Zone.track`
216
+
217
+ * Not all sources have complete metadata - missing value will be set to defaults.
218
+ * Airplay track position is not avaiable.
219
+
220
+ | Property | Description | Type | Values |
221
+ | ---------------------- | ----------- | ----------- |----------- |
222
+ | `title` | Title | `str` readonly |
223
+ | `album` | Album | `str` readonly |
224
+ | `artist` | Artist | `str` readonly |
225
+ | `genre` | Genre | `str` readonly |
226
+ | `duration` | Length in miliseconds (ms) | `int` readonly |
227
+ | `progress` | Current position in miliseconds (ms) | `int` readonly |
228
+ | `cover_art_url` | URL to cover art | `str` readonly |
229
+ | `source` | Track source e.g Spotify | `int` readonly | `TrackMetadata.Sources`
230
+ | `url` | URL of file or track | `str` readonly |
231
+
232
+
233
+ ## `Zone.input`
234
+
235
+ | Property | Description | Type | Values |
236
+ | ---------------------- | ----------- | ----------- |----------- |
237
+ | `source` | Change input source | `int` | `InputRouter.Sources`
238
+ | `priority` | Change input priority. Stream or analog in higher priority | `int` | `InputRouter.Priorities`
239
+
240
+ ```python
241
+ """Example"""
242
+ # Change zone 1 to listen to analog input 4
243
+ zone1.input.source = InputRouter.Sources.ANALOG_IN_4
244
+
245
+ # Change zone 1 to perfer analog input (local) over stream
246
+ zone1.input.priority = InputRouter.Priorities.LOCAL
247
+ ```
248
+
249
+ ## `Zone.group`
250
+
251
+ Working on A.3x but offically unsupported in x series amplifiers.
252
+
253
+ | Property | Description | Type | Values |
254
+ | ---------------------- | ----------- | ----------- |----------- |
255
+ | `source` | Zone ID of group master / source | `int` readonly | `Zone.IDs`
256
+ | `is_master` | This zone is the group master | `bool` readonly
257
+ | `add_member()` | Add zone to group / create group | `func` | `Zone.IDs`
258
+ | `remove_member()` | Remove zone from group | `func` | `Zone.IDs`
259
+ | `dissolve()` | Dissolve group / remove all members | `func` |
260
+ | `leave()` | Leave the group if a member | `func` |
261
+
262
+ ```python
263
+ """Examples"""
264
+ # Add group 2 to a group with zone 1 as master
265
+ zone1.group.add_member(Zone.IDs.ZONE_2)
266
+ # Remove zone 2 from group.
267
+ zone2.group.leave() # or
268
+ zone1.group.remove_member(Zone.IDs.ZONE_2)
269
+ # If zone 1 is a master, remove all members
270
+ zone1.group.dissolve()
271
+ ```
272
+
273
+ ## `Zone.analog_output`
274
+
275
+ | Property | Description | Type | Values |
276
+ | ---------------------- | ----------- | ----------- |----------- |
277
+ | `source` | Where the AO is routed from. i.e a zone, optical input or off | `int` | `AnalogOutput.Sources`
278
+ | `is_fixed_volume` | Fix the output volume. Output wont respond to volume control | `bool` readonly
279
+ | `is_fixed_volume_toggle()` | Toggle fixed volume | `func` |
280
+
281
+ ```python
282
+ """Examples"""
283
+ # Change analog output of zone1 to be outputting the optical input
284
+ zone1.analog_output.source = AnalogOutput.Sources.OPTICAL_IN
285
+
286
+ # Change analog output of zone1 to be outputting the zone 2 source (whatever zone 2 is using as a source)
287
+ zone1.analog_output.source = AnalogOutput.Sources.ZONE_2
288
+
289
+ # Fix the analog output volume.
290
+ zone1.analog_output.is_fixed_volume = True
291
+ ```
292
+
293
+ ## `Zone.settings`
294
+
295
+ | Property | Description | Type | Values |
296
+ | ---------------------- | ----------- | ----------- |----------- |
297
+ | `name` | Name | `str` |
298
+ | `disabled` | Disable the zone | `bool`
299
+ | `disabled_toggle()` | disable / enable | `func` |
300
+ | `mono` | Set output to mono or stereo | `int` | `ZoneSettings.StereoMono`
301
+ | `mono_toggle()` | Toggle mono or stereo | `func` |
302
+
303
+ ```python
304
+ """Examples"""
305
+ # Set name
306
+ zone1.settings.name = 'Living Room'
307
+ # Disable Zone
308
+ zone1.disabled = True
309
+ # Toggle mono output
310
+ zone1.mono_toggle()
311
+ ```
312
+
313
+ ## `Zone.settings.analog_input`
314
+
315
+ | Property | Description | Type | Values |
316
+ | ---------------------- | ----------- | ----------- |----------- |
317
+ | `name` | Name | `str` |
318
+ | `fixed_gain` | Fix the input gain to a specific value |`int` | `0...100`
319
+
320
+ ```python
321
+ """Examples"""
322
+ # Change zone1 analog input name
323
+ zone1.settings.analog_input.name = 'BluRay Player'
324
+
325
+ # Fix zone1 analog input gain to 50%.
326
+ zone1.settings.analog_input.fixed_gain = 50
327
+ ```
328
+
329
+
330
+ ## `Zone.settings.volume`
331
+
332
+ | Property | Description | Type | Values |
333
+ | ---------------------- | ----------- | ----------- |----------- |
334
+ | `default_on` | Default on volume | `int` | `0...100`
335
+ | `max_left` | Max volume left channel | `int` | `0...100`
336
+ | `max_right` | Max volume right channel | `int` | `0...100`
337
+
338
+ ```python
339
+ """Examples"""
340
+ # Set default on volume to 50%
341
+ zone1.settings.volume.default_on = 50
342
+ # Set maximum volume for left channel to 75%
343
+ zone1.settings.volume.default_on = 75
344
+ ```
345
+
346
+ ## `Zone.settings.eq`
347
+
348
+ | Property | Description | Type | Values |
349
+ | ---------------------- | ----------- | ----------- |----------- |
350
+ | `enabled` | Enable / disable EQ | `bool`
351
+
352
+ EQ be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `-10`dB to `+10`dB
353
+
354
+ | Property | Description | Type | Values |
355
+ | ---------------------- | ----------- | ----------- |----------- |
356
+ | `hz60_db` | 60Hz | `int` | `-10...10`
357
+ | `hz200_db` | 200Hz | `int` | `-10...10`
358
+ | `hz500_db` | 500Hz | `int` | `-10...10`
359
+ | `khz1_db` | 1kHz | `int` | `-10...10`
360
+ | `khz4_db` | 4kHz | `int` | `-10...10`
361
+ | `khz8_db` | 8kHz | `int` | `-10...10`
362
+ | `khz15_db` | 15kHz | `int` | `-10...10`
363
+
364
+ ```python
365
+ """Examples"""
366
+ # Set 1kHz EQ to -2
367
+ zone1.settings.eq.khz1_db = -2
368
+ ```
369
+
370
+ ## Credit
371
+
372
+ 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). VSSLs non-legacy iOS app version 1.1.3(1) is crashing my A.3x.
373
+
374
+ ## Known Issues & Limitiations
375
+
376
+ * Tested on VSSL **A.3x** software version **p15305.016.3701**
377
+ * Not tested on A.1x, A.6.x or original A series range of amplifiers (testers welcome)
378
+ * VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
379
+ * Not all sources set the volume to 0 when the zone is muted
380
+ * Grouping feedback is flaky on the X series amplifiers
381
+ * Cant stop a URL playback, feedback is worng at least
382
+ * 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
383
+ * `stop()` is intended to disconnect the client and pause the stream. Doesnt always function this way, depending on stream source
384
+ * Occasionally a zones might stop responding to certain commands, issuing the `reboot` command generally corrects
385
+