vsslctrl 0.1.3.dev1__tar.gz → 0.1.5.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 (33) hide show
  1. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/PKG-INFO +78 -21
  2. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/README.md +77 -20
  3. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/pyproject.toml +1 -1
  4. vsslctrl-0.1.5.dev1/vsslctrl/__init__.py +6 -0
  5. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/api_alpha.py +2 -2
  6. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/api_base.py +13 -0
  7. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/core.py +37 -51
  8. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/data_structure.py +193 -56
  9. vsslctrl-0.1.5.dev1/vsslctrl/device.py +115 -0
  10. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/group.py +13 -7
  11. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/io.py +12 -5
  12. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/settings.py +1 -1
  13. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/zone.py +3 -14
  14. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl.egg-info/PKG-INFO +78 -21
  15. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl.egg-info/SOURCES.txt +1 -0
  16. vsslctrl-0.1.3.dev1/vsslctrl/__init__.py +0 -4
  17. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/LICENSE +0 -0
  18. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/setup.cfg +0 -0
  19. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/tests/test_event_bus.py +0 -0
  20. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/tests/test_integration.py +0 -0
  21. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/tests/test_settings.py +0 -0
  22. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/tests/test_zone.py +0 -0
  23. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/api_bravo.py +0 -0
  24. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/decorators.py +0 -0
  25. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/discovery.py +0 -0
  26. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/event_bus.py +0 -0
  27. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/exceptions.py +0 -0
  28. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/track.py +0 -0
  29. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/transport.py +0 -0
  30. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl/utils.py +0 -0
  31. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl.egg-info/dependency_links.txt +0 -0
  32. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.dev1}/vsslctrl.egg-info/requires.txt +0 -0
  33. {vsslctrl-0.1.3.dev1 → vsslctrl-0.1.5.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.dev1
3
+ Version: 0.1.5.dev1
4
4
  Summary: Package for controlling VSSL amplifiers
5
5
  Author-email: vsslctrl <vsslcontrolled@proton.me>
6
6
  Classifier: Development Status :: 4 - Beta
@@ -28,37 +28,45 @@ Requires-Dist: zeroconf==0.132.2; extra == "optional"
28
28
 
29
29
  I am looking for testers with any VSSL amplifier models, please get in touch if you interested in helping.
30
30
 
31
- Only tested on VSSL **A.3x** software version **p15305.016.3701**.
31
+ Important
32
+ -----------
33
+ Only tested on a VSSL **A.3x** software version **p15305.016.3701**.
34
+
35
+ **Notice:** VSSLs new iOS app titled `VSSL`can cause connection refused issues if running at the same time as `vsslctrl`. Best to not use the iOS app togeather. The `VSSL Legacy` app works fine
36
+
37
+ **Warning:** no *[VSSL Agent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start)* should be running on the same network. If you dont know what this is, then you can probably ignore this notice.
32
38
 
33
39
  ## TODOs
34
40
 
35
41
  * Test on other models (hardware needed)
36
42
  * Home Assistant integration. In progress, [here](https://github.com/vsslctrl/integration.home-assistant)
37
- * Function scoping to supported feature / models
43
+ * ~~Function scoping to supported feature / models~~ (needs tests)
38
44
  * Better test coverage
39
45
 
40
- Important
41
- -----------
42
- **Warning:** no *[VSSL Agent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start)* should be running on the same network. If you dont know what this is, then you can probably ignore this notice.
43
-
44
-
45
46
  Basic Usage
46
47
  -----------
47
48
  `vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
48
49
 
49
50
  ```python
50
51
  import asyncio
51
- from vsslctrl import Vssl, Zone
52
+ from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
52
53
 
53
54
  async def main():
54
55
 
55
56
  # Represents a physical VSSL amplifier
56
57
  vssl = Vssl()
57
58
 
59
+ """Optional to init Vssl with a device model
60
+
61
+ vssl = Vssl(DeviceModels.A3X)
62
+
63
+ If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
64
+ """
65
+
58
66
  # Add each you wish to control
59
- zone1 = vssl.add_zone(Zone.IDs.ZONE_1, '192.168.1.10')
60
- zone2 = vssl.add_zone(Zone.IDs.ZONE_2, '192.168.1.11')
61
- zone3 = vssl.add_zone(Zone.IDs.ZONE_3, '192.168.1.12')
67
+ zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
68
+ zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
69
+ zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
62
70
  #... up to 6 zones
63
71
 
64
72
  # Connect and initiate zones.
@@ -110,7 +118,7 @@ print(zone_name)
110
118
  | ---------------------- | ----------- | ----------- |
111
119
  | `sw_version` | Software version | `str` readonly
112
120
  | `serial` | Serial number | `str` readonly
113
- | `model_zone_qty` | Number of zones the device has | `int` readonly
121
+ | `model` | Device Model | `int` readonly
114
122
  | `reboot()` | Reboot all zones | `func` |
115
123
 
116
124
  ```python
@@ -152,7 +160,7 @@ vssl.settings.power.adaptive = True
152
160
 
153
161
  | Property | Description | Type | Values |
154
162
  | ---------------------- | ----------- | ----------- |----------- |
155
- | `id` | Zone number / ID | `int` readonly | `Zone.IDs`
163
+ | `id` | Zone number / ID | `int` readonly | `ZoneIDs`
156
164
  | `host` | IP address | `str` readonly
157
165
  | `volume` | Volume | `int` | `0...100`
158
166
  | `volume_raise([step=1])` | Raise volume by `step` | `func` | step: `int` `1...100`
@@ -219,7 +227,7 @@ zone1.transport.state = ZoneTransport.States.PAUSE
219
227
  ## `Zone.track`
220
228
 
221
229
  * Not all sources have complete metadata - missing value will be set to defaults.
222
- * Airplay track position is not avaiable.
230
+ * Airplay track `progress` is not avaiable.
223
231
 
224
232
  | Property | Description | Type | Values |
225
233
  | ---------------------- | ----------- | ----------- |----------- |
@@ -256,20 +264,20 @@ Working on A.3x but offically unsupported in x series amplifiers.
256
264
 
257
265
  | Property | Description | Type | Values |
258
266
  | ---------------------- | ----------- | ----------- |----------- |
259
- | `source` | Zone ID of group master / source | `int` readonly | `Zone.IDs`
267
+ | `source` | Zone ID of group master / source | `int` readonly | `ZoneIDs`
260
268
  | `is_master` | This zone is the group master | `bool` readonly
261
- | `add_member()` | Add zone to group / create group | `func` | `Zone.IDs`
262
- | `remove_member()` | Remove zone from group | `func` | `Zone.IDs`
269
+ | `add_member()` | Add zone to group / create group | `func` | `ZoneIDs`
270
+ | `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
263
271
  | `dissolve()` | Dissolve group / remove all members | `func` |
264
272
  | `leave()` | Leave the group if a member | `func` |
265
273
 
266
274
  ```python
267
275
  """Examples"""
268
276
  # Add group 2 to a group with zone 1 as master
269
- zone1.group.add_member(Zone.IDs.ZONE_2)
270
- # Remove zone 2 from group.
277
+ zone1.group.add_member(ZoneIDs.ZONE_2)
278
+ # Remove zone 2 from group
271
279
  zone2.group.leave() # or
272
- zone1.group.remove_member(Zone.IDs.ZONE_2)
280
+ zone1.group.remove_member(ZoneIDs.ZONE_2)
273
281
  # If zone 1 is a master, remove all members
274
282
  zone1.group.dissolve()
275
283
  ```
@@ -371,6 +379,53 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
371
379
  zone1.settings.eq.khz1_db = -2
372
380
  ```
373
381
 
382
+ ## Another (Lite) Way
383
+
384
+ 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`.
385
+
386
+ | HEX | Description |
387
+ | ---------------------- | ----------- |
388
+ | `\x10\x05\x03\x0{Zone Number}\xff\x03` | Volume Up
389
+ | `\x10\x05\x03\x0{Zone Number}\xfe\x03` | Volume Down
390
+ | `\x10\x11\x02\x0{Zone Number}\x01` | Mute
391
+ | All commands can be found by looking [here](https://github.com/vsslctrl/vsslctrl/blob/2c43c2f2393b94bc0e062d2ab90144343eca16ef/vsslctrl/api_alpha.py) |
392
+
393
+
394
+ The `volume up` HEX command for `Zone 2` would be `\x10\x05\x03\x02\xff\x03`
395
+
396
+ Now send the raw HEX using Netcat to the device using this syntax:
397
+
398
+ `echo -e "{HEX Command}" | nc {IP Address} 50002`
399
+
400
+ For example to send `volume up` to `Zone 2`:
401
+
402
+ `echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002`
403
+
404
+ Home Assistant `Configuration.yaml` example:
405
+
406
+ ```ymal
407
+ ...
408
+
409
+ shell_command:
410
+ #Zone 1
411
+ vssl_zone_1_volume_up: 'echo -e "\x10\x05\x03\x01\xff\x03" | nc 192.168.1.10 50002'
412
+ vssl_zone_1_volume_down: 'echo -e "\x10\x05\x03\x01\xfe\x03" | nc 192.168.1.10 50002'
413
+ vssl_zone_1_mute: 'echo -e "\x10\x11\x02\x01\x01" | nc 192.168.1.10 50002'
414
+ vssl_zone_1_unmute: 'echo -e "\x10\x11\x02\x01\x00" | nc 192.168.1.10 50002'
415
+ #Zone 2
416
+ vssl_zone_2_volume_up: 'echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002'
417
+ vssl_zone_2_volume_down: 'echo -e "\x10\x05\x03\x02\xfe\x03" | nc 192.168.1.11 50002'
418
+ vssl_zone_2_mute: 'echo -e "\x10\x11\x02\x02\x01" | nc 192.168.1.11 50002'
419
+ vssl_zone_2_unmute: 'echo -e "\x10\x11\x02\x02\x00" | nc 192.168.1.11 50002'
420
+ #Zone 3
421
+ vssl_zone_3_volume_up: 'echo -e "\x10\x05\x03\x03\xff\x03" | nc 192.168.1.12 50002'
422
+ vssl_zone_3_volume_down: 'echo -e "\x10\x05\x03\x03\xfe\x03" | nc 192.168.1.12 50002'
423
+ vssl_zone_3_mute: 'echo -e "\x10\x11\x02\x03\x01" | nc 192.168.1.12 50002'
424
+ vssl_zone_3_unmute: 'echo -e "\x10\x11\x02\x03\x00" | nc 192.168.1.12 50002'
425
+
426
+ ...
427
+ ```
428
+
374
429
  ## Credit
375
430
 
376
431
  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).
@@ -382,6 +437,7 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
382
437
  * VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
383
438
  * Not all sources set the volume to 0 when the zone is muted
384
439
  * Grouping feedback is flaky on the X series amplifiers
440
+ * Airplay `Zone.track.progress` is not avaiable.
385
441
  * Cant stop a URL playback, feedback is worng at least
386
442
  * 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
387
443
  * `stop()` is intended to disconnect the client and pause the stream. Doesnt always function this way, depending on stream source
@@ -392,4 +448,5 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
392
448
  * REST API / Web App
393
449
  * Save and recall EQ
394
450
  * A.1(x) coverage i.e Bluetooth
451
+ * IR Control
395
452
 
@@ -7,37 +7,45 @@
7
7
 
8
8
  I am looking for testers with any VSSL amplifier models, please get in touch if you interested in helping.
9
9
 
10
- Only tested on VSSL **A.3x** software version **p15305.016.3701**.
10
+ Important
11
+ -----------
12
+ Only tested on a VSSL **A.3x** software version **p15305.016.3701**.
13
+
14
+ **Notice:** VSSLs new iOS app titled `VSSL`can cause connection refused issues if running at the same time as `vsslctrl`. Best to not use the iOS app togeather. The `VSSL Legacy` app works fine
15
+
16
+ **Warning:** no *[VSSL Agent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start)* should be running on the same network. If you dont know what this is, then you can probably ignore this notice.
11
17
 
12
18
  ## TODOs
13
19
 
14
20
  * Test on other models (hardware needed)
15
21
  * Home Assistant integration. In progress, [here](https://github.com/vsslctrl/integration.home-assistant)
16
- * Function scoping to supported feature / models
22
+ * ~~Function scoping to supported feature / models~~ (needs tests)
17
23
  * Better test coverage
18
24
 
19
- Important
20
- -----------
21
- **Warning:** no *[VSSL Agent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start)* should be running on the same network. If you dont know what this is, then you can probably ignore this notice.
22
-
23
-
24
25
  Basic Usage
25
26
  -----------
26
27
  `vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
27
28
 
28
29
  ```python
29
30
  import asyncio
30
- from vsslctrl import Vssl, Zone
31
+ from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
31
32
 
32
33
  async def main():
33
34
 
34
35
  # Represents a physical VSSL amplifier
35
36
  vssl = Vssl()
36
37
 
38
+ """Optional to init Vssl with a device model
39
+
40
+ vssl = Vssl(DeviceModels.A3X)
41
+
42
+ If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
43
+ """
44
+
37
45
  # Add each you wish to control
38
- zone1 = vssl.add_zone(Zone.IDs.ZONE_1, '192.168.1.10')
39
- zone2 = vssl.add_zone(Zone.IDs.ZONE_2, '192.168.1.11')
40
- zone3 = vssl.add_zone(Zone.IDs.ZONE_3, '192.168.1.12')
46
+ zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
47
+ zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
48
+ zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
41
49
  #... up to 6 zones
42
50
 
43
51
  # Connect and initiate zones.
@@ -89,7 +97,7 @@ print(zone_name)
89
97
  | ---------------------- | ----------- | ----------- |
90
98
  | `sw_version` | Software version | `str` readonly
91
99
  | `serial` | Serial number | `str` readonly
92
- | `model_zone_qty` | Number of zones the device has | `int` readonly
100
+ | `model` | Device Model | `int` readonly
93
101
  | `reboot()` | Reboot all zones | `func` |
94
102
 
95
103
  ```python
@@ -131,7 +139,7 @@ vssl.settings.power.adaptive = True
131
139
 
132
140
  | Property | Description | Type | Values |
133
141
  | ---------------------- | ----------- | ----------- |----------- |
134
- | `id` | Zone number / ID | `int` readonly | `Zone.IDs`
142
+ | `id` | Zone number / ID | `int` readonly | `ZoneIDs`
135
143
  | `host` | IP address | `str` readonly
136
144
  | `volume` | Volume | `int` | `0...100`
137
145
  | `volume_raise([step=1])` | Raise volume by `step` | `func` | step: `int` `1...100`
@@ -198,7 +206,7 @@ zone1.transport.state = ZoneTransport.States.PAUSE
198
206
  ## `Zone.track`
199
207
 
200
208
  * Not all sources have complete metadata - missing value will be set to defaults.
201
- * Airplay track position is not avaiable.
209
+ * Airplay track `progress` is not avaiable.
202
210
 
203
211
  | Property | Description | Type | Values |
204
212
  | ---------------------- | ----------- | ----------- |----------- |
@@ -235,20 +243,20 @@ Working on A.3x but offically unsupported in x series amplifiers.
235
243
 
236
244
  | Property | Description | Type | Values |
237
245
  | ---------------------- | ----------- | ----------- |----------- |
238
- | `source` | Zone ID of group master / source | `int` readonly | `Zone.IDs`
246
+ | `source` | Zone ID of group master / source | `int` readonly | `ZoneIDs`
239
247
  | `is_master` | This zone is the group master | `bool` readonly
240
- | `add_member()` | Add zone to group / create group | `func` | `Zone.IDs`
241
- | `remove_member()` | Remove zone from group | `func` | `Zone.IDs`
248
+ | `add_member()` | Add zone to group / create group | `func` | `ZoneIDs`
249
+ | `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
242
250
  | `dissolve()` | Dissolve group / remove all members | `func` |
243
251
  | `leave()` | Leave the group if a member | `func` |
244
252
 
245
253
  ```python
246
254
  """Examples"""
247
255
  # Add group 2 to a group with zone 1 as master
248
- zone1.group.add_member(Zone.IDs.ZONE_2)
249
- # Remove zone 2 from group.
256
+ zone1.group.add_member(ZoneIDs.ZONE_2)
257
+ # Remove zone 2 from group
250
258
  zone2.group.leave() # or
251
- zone1.group.remove_member(Zone.IDs.ZONE_2)
259
+ zone1.group.remove_member(ZoneIDs.ZONE_2)
252
260
  # If zone 1 is a master, remove all members
253
261
  zone1.group.dissolve()
254
262
  ```
@@ -350,6 +358,53 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
350
358
  zone1.settings.eq.khz1_db = -2
351
359
  ```
352
360
 
361
+ ## Another (Lite) Way
362
+
363
+ 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`.
364
+
365
+ | HEX | Description |
366
+ | ---------------------- | ----------- |
367
+ | `\x10\x05\x03\x0{Zone Number}\xff\x03` | Volume Up
368
+ | `\x10\x05\x03\x0{Zone Number}\xfe\x03` | Volume Down
369
+ | `\x10\x11\x02\x0{Zone Number}\x01` | Mute
370
+ | All commands can be found by looking [here](https://github.com/vsslctrl/vsslctrl/blob/2c43c2f2393b94bc0e062d2ab90144343eca16ef/vsslctrl/api_alpha.py) |
371
+
372
+
373
+ The `volume up` HEX command for `Zone 2` would be `\x10\x05\x03\x02\xff\x03`
374
+
375
+ Now send the raw HEX using Netcat to the device using this syntax:
376
+
377
+ `echo -e "{HEX Command}" | nc {IP Address} 50002`
378
+
379
+ For example to send `volume up` to `Zone 2`:
380
+
381
+ `echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002`
382
+
383
+ Home Assistant `Configuration.yaml` example:
384
+
385
+ ```ymal
386
+ ...
387
+
388
+ shell_command:
389
+ #Zone 1
390
+ vssl_zone_1_volume_up: 'echo -e "\x10\x05\x03\x01\xff\x03" | nc 192.168.1.10 50002'
391
+ vssl_zone_1_volume_down: 'echo -e "\x10\x05\x03\x01\xfe\x03" | nc 192.168.1.10 50002'
392
+ vssl_zone_1_mute: 'echo -e "\x10\x11\x02\x01\x01" | nc 192.168.1.10 50002'
393
+ vssl_zone_1_unmute: 'echo -e "\x10\x11\x02\x01\x00" | nc 192.168.1.10 50002'
394
+ #Zone 2
395
+ vssl_zone_2_volume_up: 'echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002'
396
+ vssl_zone_2_volume_down: 'echo -e "\x10\x05\x03\x02\xfe\x03" | nc 192.168.1.11 50002'
397
+ vssl_zone_2_mute: 'echo -e "\x10\x11\x02\x02\x01" | nc 192.168.1.11 50002'
398
+ vssl_zone_2_unmute: 'echo -e "\x10\x11\x02\x02\x00" | nc 192.168.1.11 50002'
399
+ #Zone 3
400
+ vssl_zone_3_volume_up: 'echo -e "\x10\x05\x03\x03\xff\x03" | nc 192.168.1.12 50002'
401
+ vssl_zone_3_volume_down: 'echo -e "\x10\x05\x03\x03\xfe\x03" | nc 192.168.1.12 50002'
402
+ vssl_zone_3_mute: 'echo -e "\x10\x11\x02\x03\x01" | nc 192.168.1.12 50002'
403
+ vssl_zone_3_unmute: 'echo -e "\x10\x11\x02\x03\x00" | nc 192.168.1.12 50002'
404
+
405
+ ...
406
+ ```
407
+
353
408
  ## Credit
354
409
 
355
410
  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).
@@ -361,6 +416,7 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
361
416
  * VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
362
417
  * Not all sources set the volume to 0 when the zone is muted
363
418
  * Grouping feedback is flaky on the X series amplifiers
419
+ * Airplay `Zone.track.progress` is not avaiable.
364
420
  * Cant stop a URL playback, feedback is worng at least
365
421
  * 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
366
422
  * `stop()` is intended to disconnect the client and pause the stream. Doesnt always function this way, depending on stream source
@@ -371,4 +427,5 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
371
427
  * REST API / Web App
372
428
  * Save and recall EQ
373
429
  * A.1(x) coverage i.e Bluetooth
430
+ * IR Control
374
431
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vsslctrl"
7
- version = "0.1.3.dev1"
7
+ version = "0.1.5.dev1"
8
8
  description = "Package for controlling VSSL amplifiers"
9
9
  authors = [
10
10
  { name="vsslctrl", email="vsslcontrolled@proton.me" },
@@ -0,0 +1,6 @@
1
+ from .core import Vssl
2
+ from .zone import Zone
3
+ from .data_structure import ZoneIDs
4
+ from .device import Models as DeviceModels
5
+
6
+ VSSL_NAME = "VSSL"
@@ -558,8 +558,8 @@ class APIAlpha(APIBase):
558
558
  def response_action_00_00(self, metadata: list):
559
559
  self._log_debug(f"Received 00 Status: {metadata}")
560
560
 
561
- # Workout how many zones we have
562
- self.vssl._infer_model_zone_qty(metadata)
561
+ # Guess device model
562
+ self.vssl._infer_device_model(metadata)
563
563
 
564
564
  # Analog output source
565
565
  key = DeviceStatusExtKeys.add_zone_to_bus_key(self.zone.id)
@@ -158,6 +158,19 @@ class APIBase(ABC):
158
158
 
159
159
  except asyncio.CancelledError:
160
160
  self._log_debug(f"writer close timeout")
161
+ except asyncio.TimeoutError as e:
162
+ self._log_error(f"Timeout while closing connection: {e}")
163
+ # Handle the timeout: log, retry, or take other actions
164
+ except ConnectionResetError as e:
165
+ self._log_error(f"Connection reset by peer: {e}")
166
+ # Handle the error: log, retry, or take other actions
167
+ except Exception as e:
168
+ self._log_error(f"Unexpected error occurred while disconnecting: {e}")
169
+ # Handle unexpected errors
170
+ finally:
171
+ self._writer = None
172
+
173
+ self._reader = None
161
174
 
162
175
  self._log_info(f"{self.host}:{self.port}: disconnected")
163
176
 
@@ -10,7 +10,8 @@ from .event_bus import EventBus
10
10
  from .settings import VsslSettings
11
11
  from .decorators import logging_helpers
12
12
  from .discovery import check_zeroconf_availability, fetch_zone_id_serial
13
- from .data_structure import DeviceModels
13
+ from .device import Models
14
+ from .data_structure import ZoneIDs
14
15
 
15
16
 
16
17
  @logging_helpers("VSSL:")
@@ -23,14 +24,13 @@ class Vssl:
23
24
  class Events:
24
25
  PREFIX = "vssl."
25
26
  MODEL_CHANGE = PREFIX + "model_changed"
26
- MODEL_ZONE_QTY_CHANGE = PREFIX + "model_zone_qty_changed"
27
27
  SW_VERSION_CHANGE = PREFIX + "sw_version_changed"
28
28
  SERIAL_CHANGE = PREFIX + "serial_changed"
29
29
  ALL = EventBus.WILDCARD
30
30
 
31
31
  def __init__(
32
32
  self,
33
- model: DeviceModels = None,
33
+ model: Models = None,
34
34
  zones: Union[str, List[str]] = None,
35
35
  ):
36
36
  self.event_bus = EventBus()
@@ -38,10 +38,10 @@ class Vssl:
38
38
  self._sw_version = None
39
39
  self._serial = None
40
40
  self._model = None
41
- self._model_zone_qty = 0
42
41
  self.settings = VsslSettings(self)
43
42
 
44
- self.model = model
43
+ if model is not None:
44
+ self.model = model
45
45
 
46
46
  # Add zones if any are passed
47
47
  if zones:
@@ -62,22 +62,22 @@ class Vssl:
62
62
  try:
63
63
  key, first_zone = zones_to_init.popitem()
64
64
 
65
- # If we pass a model to the zone, the zone count will be worked out from that,
66
- # otherwise we will try and work it out once we receive some info about he device
67
- if not self.model_zone_qty:
68
- future_model_zone_qty = self.event_bus.future(
69
- self.Events.MODEL_ZONE_QTY_CHANGE, self.ENTITY_ID
65
+ # If we dont pass a model we will default to the zone count of the X series amps once we
66
+ # know some info about the device
67
+ if not self.model:
68
+ future_model = self.event_bus.future(
69
+ self.Events.MODEL_CHANGE, self.ENTITY_ID
70
70
  )
71
71
 
72
72
  # Lets make sure the zone is initialised, otherwsie we fail all
73
73
  await first_zone.initialise()
74
74
 
75
- # Only continue after we now how many zones the device supports
75
+ # Only continue after we have a model
76
76
  try:
77
- if not self.model_zone_qty:
78
- await asyncio.wait_for(future_model_zone_qty, timeout=init_timeout)
77
+ if not self.model:
78
+ await asyncio.wait_for(future_model, timeout=init_timeout)
79
79
 
80
- if len(self.zones) > self.model_zone_qty:
80
+ if len(self.zones) > self.model.zone_count:
81
81
  raise VsslCtrlException("")
82
82
 
83
83
  except asyncio.TimeoutError:
@@ -87,7 +87,8 @@ class Vssl:
87
87
  raise VsslCtrlException(message)
88
88
 
89
89
  except VsslCtrlException:
90
- message = f"Device model only has {self.model_zone_qty} zones instead of {len(self.zones)}"
90
+ zone_count = self.model.zone_count
91
+ message = f"Device model only has {zone_count} zones instead of {len(self.zones)}"
91
92
  self._log_critical(message)
92
93
  await first_zone.disconnect()
93
94
  raise VsslCtrlException(message)
@@ -173,47 +174,32 @@ class Vssl:
173
174
  return self._model
174
175
 
175
176
  @model.setter
176
- def model(self, model: str):
177
- if self.model != model:
178
- if DeviceModels.is_valid(model):
179
- self._set_property("model", DeviceModels(model))
180
- else:
181
- message = f"DeviceModels {model} doesnt exist"
182
- self._log_error(message)
183
- raise VsslCtrlException(message)
184
-
185
- #
186
- # The amount of zones this VSSL has. This is not how many zones have been initialised
187
- # but how many zones the model has in total. We can have 1, 3 or 6 zones.
188
- #
189
- @property
190
- def model_zone_qty(self):
191
- if not self._model_zone_qty and self.model is not None:
192
- self._model_zone_qty = DeviceModels.zone_count(self.model.value)
193
-
194
- return self._model_zone_qty
195
-
196
- @model_zone_qty.setter
197
- def model_zone_qty(self, model_zone_qty: int):
198
- pass # read-only
177
+ def model(self, model):
178
+ if Models.is_valid(model):
179
+ self._set_property("model", Models(model).value)
180
+ elif isinstance(model, str):
181
+ model = model.upper()
182
+ if hasattr(Models, model):
183
+ self._set_property("model", getattr(Models, model).value)
184
+ else:
185
+ message = f"Model {model} doesnt exist"
186
+ self._log_error(message)
187
+ raise VsslCtrlException(message)
199
188
 
200
189
  #
201
- # Work out the model_zone_qty given device info
190
+ # Work out the a model given some device info
202
191
  #
203
- def _infer_model_zone_qty(self, data: Dict[str, int]):
204
- if not self.model_zone_qty:
192
+ def _infer_device_model(self, data: Dict[str, int]):
193
+ # if we dont have a model, default to x series
194
+ if not self.model:
205
195
  zone_count = sum(
206
196
  1 for key in data if key.startswith("B") and key.endswith("Src")
207
197
  )
208
198
 
209
199
  if zone_count in (1, 3, 6):
210
- self._model_zone_qty = zone_count
200
+ self.model = f"A{zone_count}X"
211
201
  else:
212
- self._model_zone_qty = 1
213
-
214
- self.event_bus.publish(
215
- self.Events.MODEL_ZONE_QTY_CHANGE, self.ENTITY_ID, self.model_zone_qty
216
- )
202
+ self.model = Models.A1X
217
203
 
218
204
  #
219
205
  # Disconnect / Shutdown
@@ -234,9 +220,9 @@ class Vssl:
234
220
  #
235
221
  # Add a Zone
236
222
  #
237
- def add_zone(self, zone_index: "Zone.IDs", host: str):
238
- if Zone.IDs.is_not_valid(zone_index):
239
- error = f"Zone.IDs {zone_index} doesnt exist"
223
+ def add_zone(self, zone_index: ZoneIDs, host: str):
224
+ if ZoneIDs.is_not_valid(zone_index):
225
+ error = f"ZoneIDs {zone_index} doesnt exist"
240
226
  self._log_error(error)
241
227
  raise ZoneError(error)
242
228
  return None
@@ -262,7 +248,7 @@ class Vssl:
262
248
  #
263
249
  # Get a Zone by ID
264
250
  #
265
- def get_zone(self, zone_index: "Zone.IDs"):
251
+ def get_zone(self, zone_index: ZoneIDs):
266
252
  if zone_index in self.zones:
267
253
  return self.zones[zone_index]
268
254
  else: