vsslctrl 0.1.2.dev1__tar.gz → 0.1.4.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.2.dev1 → vsslctrl-0.1.4.dev1}/PKG-INFO +74 -20
  2. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/README.md +73 -19
  3. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/pyproject.toml +1 -1
  4. vsslctrl-0.1.4.dev1/vsslctrl/__init__.py +6 -0
  5. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/api_alpha.py +2 -2
  6. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/core.py +37 -51
  7. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/data_structure.py +12 -18
  8. vsslctrl-0.1.4.dev1/vsslctrl/device.py +115 -0
  9. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/group.py +13 -7
  10. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/io.py +11 -4
  11. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/settings.py +1 -1
  12. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/track.py +8 -6
  13. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/zone.py +3 -14
  14. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl.egg-info/PKG-INFO +74 -20
  15. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl.egg-info/SOURCES.txt +1 -0
  16. vsslctrl-0.1.2.dev1/vsslctrl/__init__.py +0 -4
  17. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/LICENSE +0 -0
  18. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/setup.cfg +0 -0
  19. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/tests/test_event_bus.py +0 -0
  20. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/tests/test_integration.py +0 -0
  21. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/tests/test_settings.py +0 -0
  22. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/tests/test_zone.py +0 -0
  23. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/api_base.py +0 -0
  24. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/api_bravo.py +0 -0
  25. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/decorators.py +0 -0
  26. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/discovery.py +0 -0
  27. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/event_bus.py +0 -0
  28. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/exceptions.py +0 -0
  29. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/transport.py +0 -0
  30. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl/utils.py +0 -0
  31. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl.egg-info/dependency_links.txt +0 -0
  32. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.dev1}/vsslctrl.egg-info/requires.txt +0 -0
  33. {vsslctrl-0.1.2.dev1 → vsslctrl-0.1.4.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.2.dev1
3
+ Version: 0.1.4.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,43 @@ 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
+ **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
36
 
33
37
  ## TODOs
34
38
 
35
39
  * Test on other models (hardware needed)
36
40
  * Home Assistant integration. In progress, [here](https://github.com/vsslctrl/integration.home-assistant)
37
- * Function scoping to supported feature / models
41
+ * ~~Function scoping to supported feature / models~~ (needs tests)
38
42
  * Better test coverage
39
43
 
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
44
  Basic Usage
46
45
  -----------
47
46
  `vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
48
47
 
49
48
  ```python
50
49
  import asyncio
51
- from vsslctrl import Vssl, Zone
50
+ from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
52
51
 
53
52
  async def main():
54
53
 
55
54
  # Represents a physical VSSL amplifier
56
55
  vssl = Vssl()
57
56
 
57
+ """Optional to init Vssl with a device model
58
+
59
+ vssl = Vssl(DeviceModels.A3X)
60
+
61
+ If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
62
+ """
63
+
58
64
  # 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')
65
+ zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
66
+ zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
67
+ zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
62
68
  #... up to 6 zones
63
69
 
64
70
  # Connect and initiate zones.
@@ -110,7 +116,7 @@ print(zone_name)
110
116
  | ---------------------- | ----------- | ----------- |
111
117
  | `sw_version` | Software version | `str` readonly
112
118
  | `serial` | Serial number | `str` readonly
113
- | `model_zone_qty` | Number of zones the device has | `int` readonly
119
+ | `model` | Device Model | `int` readonly
114
120
  | `reboot()` | Reboot all zones | `func` |
115
121
 
116
122
  ```python
@@ -152,7 +158,7 @@ vssl.settings.power.adaptive = True
152
158
 
153
159
  | Property | Description | Type | Values |
154
160
  | ---------------------- | ----------- | ----------- |----------- |
155
- | `id` | Zone number / ID | `int` readonly | `Zone.IDs`
161
+ | `id` | Zone number / ID | `int` readonly | `ZoneIDs`
156
162
  | `host` | IP address | `str` readonly
157
163
  | `volume` | Volume | `int` | `0...100`
158
164
  | `volume_raise([step=1])` | Raise volume by `step` | `func` | step: `int` `1...100`
@@ -256,20 +262,20 @@ Working on A.3x but offically unsupported in x series amplifiers.
256
262
 
257
263
  | Property | Description | Type | Values |
258
264
  | ---------------------- | ----------- | ----------- |----------- |
259
- | `source` | Zone ID of group master / source | `int` readonly | `Zone.IDs`
265
+ | `source` | Zone ID of group master / source | `int` readonly | `ZoneIDs`
260
266
  | `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`
267
+ | `add_member()` | Add zone to group / create group | `func` | `ZoneIDs`
268
+ | `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
263
269
  | `dissolve()` | Dissolve group / remove all members | `func` |
264
270
  | `leave()` | Leave the group if a member | `func` |
265
271
 
266
272
  ```python
267
273
  """Examples"""
268
274
  # 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.
275
+ zone1.group.add_member(ZoneIDs.ZONE_2)
276
+ # Remove zone 2 from group
271
277
  zone2.group.leave() # or
272
- zone1.group.remove_member(Zone.IDs.ZONE_2)
278
+ zone1.group.remove_member(ZoneIDs.ZONE_2)
273
279
  # If zone 1 is a master, remove all members
274
280
  zone1.group.dissolve()
275
281
  ```
@@ -371,6 +377,53 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
371
377
  zone1.settings.eq.khz1_db = -2
372
378
  ```
373
379
 
380
+ ## Another (Lite) Way
381
+
382
+ 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 ant network tool) on port `50002`.
383
+
384
+ | HEX | Description |
385
+ | ---------------------- | ----------- |
386
+ | `\x10\x05\x03\x0{Zone Number}\xff\x03` | Volume Up
387
+ | `\x10\x05\x03\x0{Zone Number}\xfe\x03` | Volume Down
388
+ | `\x10\x11\x02\x0{Zone Number}\x01` | Mute
389
+ | All commands can be found by looking [here](https://github.com/vsslctrl/vsslctrl/blob/2c43c2f2393b94bc0e062d2ab90144343eca16ef/vsslctrl/api_alpha.py) |
390
+
391
+
392
+ The `volume up` HEX command for `Zone 2` would be `\x10\x05\x03\x02\xff\x03`
393
+
394
+ Now send the raw HEX using Netcat to the device using this syntax:
395
+
396
+ `echo -e "{HEX Command}" | nc {IP Address} 50002`
397
+
398
+ For example to send `volume up` to `Zone 2`:
399
+
400
+ `echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002`
401
+
402
+ Home Assistant `Configuration.yaml` example:
403
+
404
+ ```ymal
405
+ ...
406
+
407
+ shell_command:
408
+ #Zone 1
409
+ vssl_zone_1_volume_up: 'echo -e "\x10\x05\x03\x01\xff\x03" | nc 192.168.1.10 50002'
410
+ vssl_zone_1_volume_down: 'echo -e "\x10\x05\x03\x01\xfe\x03" | nc 192.168.1.10 50002'
411
+ vssl_zone_1_mute: 'echo -e "\x10\x11\x02\x01\x01" | nc 192.168.1.10 50002'
412
+ vssl_zone_1_unmute: 'echo -e "\x10\x11\x02\x01\x00" | nc 192.168.1.10 50002'
413
+ #Zone 2
414
+ vssl_zone_2_volume_up: 'echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002'
415
+ vssl_zone_2_volume_down: 'echo -e "\x10\x05\x03\x02\xfe\x03" | nc 192.168.1.11 50002'
416
+ vssl_zone_2_mute: 'echo -e "\x10\x11\x02\x02\x01" | nc 192.168.1.11 50002'
417
+ vssl_zone_2_unmute: 'echo -e "\x10\x11\x02\x02\x00" | nc 192.168.1.11 50002'
418
+ #Zone 3
419
+ vssl_zone_3_volume_up: 'echo -e "\x10\x05\x03\x03\xff\x03" | nc 192.168.1.12 50002'
420
+ vssl_zone_3_volume_down: 'echo -e "\x10\x05\x03\x03\xfe\x03" | nc 192.168.1.12 50002'
421
+ vssl_zone_3_mute: 'echo -e "\x10\x11\x02\x03\x01" | nc 192.168.1.12 50002'
422
+ vssl_zone_3_unmute: 'echo -e "\x10\x11\x02\x03\x00" | nc 192.168.1.12 50002'
423
+
424
+ ...
425
+ ```
426
+
374
427
  ## Credit
375
428
 
376
429
  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).
@@ -392,4 +445,5 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
392
445
  * REST API / Web App
393
446
  * Save and recall EQ
394
447
  * A.1(x) coverage i.e Bluetooth
448
+ * IR Control
395
449
 
@@ -7,37 +7,43 @@
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
+ **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
15
 
12
16
  ## TODOs
13
17
 
14
18
  * Test on other models (hardware needed)
15
19
  * Home Assistant integration. In progress, [here](https://github.com/vsslctrl/integration.home-assistant)
16
- * Function scoping to supported feature / models
20
+ * ~~Function scoping to supported feature / models~~ (needs tests)
17
21
  * Better test coverage
18
22
 
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
23
  Basic Usage
25
24
  -----------
26
25
  `vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
27
26
 
28
27
  ```python
29
28
  import asyncio
30
- from vsslctrl import Vssl, Zone
29
+ from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
31
30
 
32
31
  async def main():
33
32
 
34
33
  # Represents a physical VSSL amplifier
35
34
  vssl = Vssl()
36
35
 
36
+ """Optional to init Vssl with a device model
37
+
38
+ vssl = Vssl(DeviceModels.A3X)
39
+
40
+ If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
41
+ """
42
+
37
43
  # 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')
44
+ zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
45
+ zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
46
+ zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
41
47
  #... up to 6 zones
42
48
 
43
49
  # Connect and initiate zones.
@@ -89,7 +95,7 @@ print(zone_name)
89
95
  | ---------------------- | ----------- | ----------- |
90
96
  | `sw_version` | Software version | `str` readonly
91
97
  | `serial` | Serial number | `str` readonly
92
- | `model_zone_qty` | Number of zones the device has | `int` readonly
98
+ | `model` | Device Model | `int` readonly
93
99
  | `reboot()` | Reboot all zones | `func` |
94
100
 
95
101
  ```python
@@ -131,7 +137,7 @@ vssl.settings.power.adaptive = True
131
137
 
132
138
  | Property | Description | Type | Values |
133
139
  | ---------------------- | ----------- | ----------- |----------- |
134
- | `id` | Zone number / ID | `int` readonly | `Zone.IDs`
140
+ | `id` | Zone number / ID | `int` readonly | `ZoneIDs`
135
141
  | `host` | IP address | `str` readonly
136
142
  | `volume` | Volume | `int` | `0...100`
137
143
  | `volume_raise([step=1])` | Raise volume by `step` | `func` | step: `int` `1...100`
@@ -235,20 +241,20 @@ Working on A.3x but offically unsupported in x series amplifiers.
235
241
 
236
242
  | Property | Description | Type | Values |
237
243
  | ---------------------- | ----------- | ----------- |----------- |
238
- | `source` | Zone ID of group master / source | `int` readonly | `Zone.IDs`
244
+ | `source` | Zone ID of group master / source | `int` readonly | `ZoneIDs`
239
245
  | `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`
246
+ | `add_member()` | Add zone to group / create group | `func` | `ZoneIDs`
247
+ | `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
242
248
  | `dissolve()` | Dissolve group / remove all members | `func` |
243
249
  | `leave()` | Leave the group if a member | `func` |
244
250
 
245
251
  ```python
246
252
  """Examples"""
247
253
  # 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.
254
+ zone1.group.add_member(ZoneIDs.ZONE_2)
255
+ # Remove zone 2 from group
250
256
  zone2.group.leave() # or
251
- zone1.group.remove_member(Zone.IDs.ZONE_2)
257
+ zone1.group.remove_member(ZoneIDs.ZONE_2)
252
258
  # If zone 1 is a master, remove all members
253
259
  zone1.group.dissolve()
254
260
  ```
@@ -350,6 +356,53 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
350
356
  zone1.settings.eq.khz1_db = -2
351
357
  ```
352
358
 
359
+ ## Another (Lite) Way
360
+
361
+ 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 ant network tool) on port `50002`.
362
+
363
+ | HEX | Description |
364
+ | ---------------------- | ----------- |
365
+ | `\x10\x05\x03\x0{Zone Number}\xff\x03` | Volume Up
366
+ | `\x10\x05\x03\x0{Zone Number}\xfe\x03` | Volume Down
367
+ | `\x10\x11\x02\x0{Zone Number}\x01` | Mute
368
+ | All commands can be found by looking [here](https://github.com/vsslctrl/vsslctrl/blob/2c43c2f2393b94bc0e062d2ab90144343eca16ef/vsslctrl/api_alpha.py) |
369
+
370
+
371
+ The `volume up` HEX command for `Zone 2` would be `\x10\x05\x03\x02\xff\x03`
372
+
373
+ Now send the raw HEX using Netcat to the device using this syntax:
374
+
375
+ `echo -e "{HEX Command}" | nc {IP Address} 50002`
376
+
377
+ For example to send `volume up` to `Zone 2`:
378
+
379
+ `echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002`
380
+
381
+ Home Assistant `Configuration.yaml` example:
382
+
383
+ ```ymal
384
+ ...
385
+
386
+ shell_command:
387
+ #Zone 1
388
+ vssl_zone_1_volume_up: 'echo -e "\x10\x05\x03\x01\xff\x03" | nc 192.168.1.10 50002'
389
+ vssl_zone_1_volume_down: 'echo -e "\x10\x05\x03\x01\xfe\x03" | nc 192.168.1.10 50002'
390
+ vssl_zone_1_mute: 'echo -e "\x10\x11\x02\x01\x01" | nc 192.168.1.10 50002'
391
+ vssl_zone_1_unmute: 'echo -e "\x10\x11\x02\x01\x00" | nc 192.168.1.10 50002'
392
+ #Zone 2
393
+ vssl_zone_2_volume_up: 'echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002'
394
+ vssl_zone_2_volume_down: 'echo -e "\x10\x05\x03\x02\xfe\x03" | nc 192.168.1.11 50002'
395
+ vssl_zone_2_mute: 'echo -e "\x10\x11\x02\x02\x01" | nc 192.168.1.11 50002'
396
+ vssl_zone_2_unmute: 'echo -e "\x10\x11\x02\x02\x00" | nc 192.168.1.11 50002'
397
+ #Zone 3
398
+ vssl_zone_3_volume_up: 'echo -e "\x10\x05\x03\x03\xff\x03" | nc 192.168.1.12 50002'
399
+ vssl_zone_3_volume_down: 'echo -e "\x10\x05\x03\x03\xfe\x03" | nc 192.168.1.12 50002'
400
+ vssl_zone_3_mute: 'echo -e "\x10\x11\x02\x03\x01" | nc 192.168.1.12 50002'
401
+ vssl_zone_3_unmute: 'echo -e "\x10\x11\x02\x03\x00" | nc 192.168.1.12 50002'
402
+
403
+ ...
404
+ ```
405
+
353
406
  ## Credit
354
407
 
355
408
  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).
@@ -371,4 +424,5 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
371
424
  * REST API / Web App
372
425
  * Save and recall EQ
373
426
  * A.1(x) coverage i.e Bluetooth
427
+ * IR Control
374
428
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vsslctrl"
7
- version = "0.1.2.dev1"
7
+ version = "0.1.4.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)
@@ -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:
@@ -29,24 +29,18 @@ class VsslIntEnum(VsslEnum, IntEnum):
29
29
  """IntEnum"""
30
30
 
31
31
 
32
- class DeviceModels(VsslEnum):
33
- A1 = "a1"
34
- A3 = "a3"
35
- A6 = "a6"
36
- A1X = "a1x"
37
- A3X = "a3x"
38
- A6X = "a6x"
39
-
40
- @staticmethod
41
- # Get the amount of zones the model has by pulling out the number in the model
42
- def zone_count(model: str):
43
- if DeviceModels.is_valid(model.lower()):
44
- pattern = r"\d+"
45
- match = re.search(pattern, model)
46
- if match:
47
- number = int(match.group())
48
- return number
49
- return 1
32
+ #
33
+ # Zones IDs
34
+ #
35
+ # Moved here to help with circular imports
36
+ #
37
+ class ZoneIDs(VsslIntEnum):
38
+ ZONE_1 = 1
39
+ ZONE_2 = 2
40
+ ZONE_3 = 3
41
+ ZONE_4 = 4
42
+ ZONE_5 = 5
43
+ ZONE_6 = 6
50
44
 
51
45
 
52
46
  """ JSON Structure
@@ -0,0 +1,115 @@
1
+ from .data_structure import VsslEnum, VsslIntEnum, ZoneIDs
2
+ from .io import AnalogOutput, InputRouter
3
+
4
+ SINGLE_ZONE = [ZoneIDs.ZONE_1]
5
+ THREE_ZONES = [ZoneIDs.ZONE_1, ZoneIDs.ZONE_2, ZoneIDs.ZONE_3]
6
+ SIX_ZONES = list(ZoneIDs)
7
+
8
+ INPUT_SOURCES_FOR_1_ZONE_DEVICE = [
9
+ InputRouter.Sources.STREAM,
10
+ InputRouter.Sources.ANALOG_IN_1,
11
+ InputRouter.Sources.OPTICAL_IN,
12
+ ]
13
+
14
+ INPUT_SOURCES_FOR_3_ZONE_DEVICE = INPUT_SOURCES_FOR_1_ZONE_DEVICE + [
15
+ InputRouter.Sources.ANALOG_IN_2,
16
+ InputRouter.Sources.ANALOG_IN_3,
17
+ ]
18
+
19
+ INPUT_SOURCES_FOR_6_ZONE_DEVICE = list(InputRouter.Sources)
20
+
21
+ ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE = [
22
+ AnalogOutput.Sources.OFF,
23
+ AnalogOutput.Sources.ZONE_1,
24
+ AnalogOutput.Sources.OPTICAL_IN,
25
+ ]
26
+
27
+ ANALOG_OUTPUT_SOURCES_FOR_3_ZONE_DEVICE = ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE + [
28
+ AnalogOutput.Sources.ZONE_2,
29
+ AnalogOutput.Sources.ZONE_3,
30
+ ]
31
+
32
+ ANALOG_OUTPUT_SOURCES_FOR_6_ZONE_DEVICE = list(AnalogOutput.Sources)
33
+
34
+
35
+ class Features(VsslIntEnum):
36
+ GROUPING = 1000
37
+ BLUETOOTH = 1001
38
+ PARTY_MODE = 1002
39
+
40
+
41
+ class Model:
42
+ def __init__(self, model: dict):
43
+ self.name = model.get("name")
44
+ self.zones = model.get("zones", [])
45
+ self.input_sources = model.get("input_sources", [])
46
+ self.analog_output_sources = model.get("analog_output_sources", [])
47
+ self.features = model.get("features", [])
48
+
49
+ @property
50
+ def zone_count(self):
51
+ return len(self.zones)
52
+
53
+ @property
54
+ def is_multizone(self):
55
+ return self.zone_count > 1
56
+
57
+ def supports_feature(self, feature: Features):
58
+ return feature in self.features
59
+
60
+
61
+ class Models(VsslEnum):
62
+ A1 = Model(
63
+ {
64
+ "name": "A1",
65
+ "zones": SINGLE_ZONE,
66
+ "input_sources": INPUT_SOURCES_FOR_1_ZONE_DEVICE,
67
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE,
68
+ "features": [Features.BLUETOOTH],
69
+ }
70
+ )
71
+ A3 = Model(
72
+ {
73
+ "name": "A3",
74
+ "zones": THREE_ZONES,
75
+ "input_sources": INPUT_SOURCES_FOR_3_ZONE_DEVICE,
76
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_3_ZONE_DEVICE,
77
+ "features": [Features.GROUPING, Features.PARTY_MODE],
78
+ }
79
+ )
80
+ A6 = Model(
81
+ {
82
+ "name": "A6",
83
+ "zones": SIX_ZONES,
84
+ "input_sources": INPUT_SOURCES_FOR_6_ZONE_DEVICE,
85
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_6_ZONE_DEVICE,
86
+ "features": [Features.GROUPING, Features.PARTY_MODE],
87
+ }
88
+ )
89
+ A1X = Model(
90
+ {
91
+ "name": "A.1x",
92
+ "zones": SINGLE_ZONE,
93
+ "input_sources": INPUT_SOURCES_FOR_1_ZONE_DEVICE,
94
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE,
95
+ "features": [Features.GROUPING, Features.BLUETOOTH],
96
+ }
97
+ )
98
+ A3X = Model(
99
+ {
100
+ "name": "A.3x",
101
+ "zones": THREE_ZONES,
102
+ "input_sources": INPUT_SOURCES_FOR_3_ZONE_DEVICE,
103
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_3_ZONE_DEVICE,
104
+ "features": [Features.GROUPING],
105
+ }
106
+ )
107
+ A6X = Model(
108
+ {
109
+ "name": "A.6x",
110
+ "zones": SIX_ZONES,
111
+ "input_sources": INPUT_SOURCES_FOR_6_ZONE_DEVICE,
112
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_6_ZONE_DEVICE,
113
+ "features": [Features.GROUPING],
114
+ }
115
+ )
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
  from . import zone
3
3
  from typing import Dict, Union
4
- from .data_structure import ZoneDataClass
4
+ from .data_structure import ZoneDataClass, ZoneIDs
5
+ from .device import Features as DeviceFeatures
5
6
 
6
7
 
7
8
  """
@@ -58,12 +59,19 @@ class ZoneGroup(ZoneDataClass):
58
59
  #
59
60
  # Group Add Zone
60
61
  #
61
- def add_member(self, zone_id: "zone.Zone.IDs"):
62
+ def add_member(self, zone_id: ZoneIDs):
63
+ # Check this device is a multizone device
64
+ if not self.zone.vssl.model.supports_feature(DeviceFeatures.GROUPING):
65
+ self.zone._log_error(
66
+ f"VSSL {self.zone.vssl.model.name} doesnt support grouping"
67
+ )
68
+ return False
69
+
62
70
  if self.zone.id == zone_id:
63
71
  self.zone._log_error(f"Zone {zone_id} cant be parent and member")
64
72
  return False
65
73
 
66
- if zone.Zone.IDs.is_not_valid(zone_id):
74
+ if ZoneIDs.is_not_valid(zone_id):
67
75
  self.zone._log_error(f"Zone {zone_id} doesnt exist")
68
76
  return False
69
77
 
@@ -84,7 +92,7 @@ class ZoneGroup(ZoneDataClass):
84
92
  #
85
93
  # Group Remove Child
86
94
  #
87
- def remove_member(self, zone_id: "zone.Zone.IDs"):
95
+ def remove_member(self, zone_id: ZoneIDs):
88
96
  self.zone.api_alpha.request_action_4B_remove(zone_id)
89
97
 
90
98
  #
@@ -132,9 +140,7 @@ class ZoneGroup(ZoneDataClass):
132
140
  pass # read-only
133
141
 
134
142
  def _set_source(self, grs: int):
135
- new_source = (
136
- zone.Zone.IDs(grs) if grs != 255 and zone.Zone.IDs.is_valid(grs) else None
137
- )
143
+ new_source = ZoneIDs(grs) if grs != 255 and ZoneIDs.is_valid(grs) else None
138
144
 
139
145
  if self.source != new_source:
140
146
  self._source = new_source
@@ -83,10 +83,14 @@ class InputRouter(ZoneDataClass):
83
83
 
84
84
  @source.setter
85
85
  def source(self, src: "InputRouter.Sources"):
86
- if self.Sources.is_valid(src):
86
+ # if self.Sources.is_valid():
87
+ if src in self.zone.vssl.model.input_sources:
88
+ # check source is avaialbe on this device
87
89
  self.zone.api_alpha.request_action_03(src)
88
90
  else:
89
- self.zone._log_error(f"InputRouter.Sources {src} doesnt exist")
91
+ self.zone._log_error(
92
+ f"InputRouter.Sources {src} doesnt exist in {list(self.zone.vssl.model.input_sources)}"
93
+ )
90
94
 
91
95
  def _set_source(self, src: int):
92
96
  if self.source != src:
@@ -160,10 +164,13 @@ class AnalogOutput(ZoneDataClass):
160
164
 
161
165
  @source.setter
162
166
  def source(self, src: "AnalogOutput.Sources"):
163
- if self.Sources.is_valid(src):
167
+ # if self.Sources.is_valid(src):
168
+ if src in self.zone.vssl.model.analog_output_sources:
164
169
  self.zone.api_alpha.request_action_1D(src)
165
170
  else:
166
- self.zone._log_error(f"AnalogOutput.Sources {src} doesnt exist")
171
+ self.zone._log_error(
172
+ f"AnalogOutput.Sources {src} doesnt exist in {list(self.zone.vssl.model.analog_output_sources)}"
173
+ )
167
174
 
168
175
  def _set_source(self, src: int):
169
176
  if self.source != src:
@@ -162,7 +162,7 @@ class ZoneSettings(ZoneDataClass):
162
162
 
163
163
  self._disabled = False
164
164
  self._name = f"Zone {zone.id}"
165
- self._mono = False
165
+ self._mono = self.StereoMono.Stereo
166
166
 
167
167
  self.eq = EQSettings(zone)
168
168
  self.volume = VolumeSettings(zone)
@@ -299,12 +299,14 @@ class TrackMetadata(ZoneDataClass):
299
299
  default_host += "/"
300
300
 
301
301
  # Lets add a query string to the coverart.jpg URL so the latest coverart
302
- # will always be downloaded if the album changes
303
- if url == self.AIRPLAY_COVER_ART and self.album:
304
- encoded_album = (
305
- base64.urlsafe_b64encode(self.album.encode()).decode().rstrip("=")
306
- )[:6]
307
- query = f"?album={encoded_album}"
302
+ # will always be downloaded if the album + artist changes
303
+ if url == self.AIRPLAY_COVER_ART and self.artist and self.album:
304
+ aahash = (
305
+ base64.urlsafe_b64encode(self.artist.encode() + self.album.encode())
306
+ .decode()
307
+ .rstrip("=")
308
+ )
309
+ query = f"?aahash={aahash}"
308
310
 
309
311
  return default_host + url + query
310
312
 
@@ -13,7 +13,7 @@ from . import core
13
13
  from .api_alpha import APIAlpha
14
14
  from .api_bravo import APIBravo
15
15
  from .utils import RepeatTimer, clamp_volume
16
- from .data_structure import DeviceModels, VsslIntEnum
16
+ from .data_structure import VsslIntEnum, ZoneIDs
17
17
  from .track import TrackMetadata
18
18
  from .io import AnalogOutput, InputRouter
19
19
  from .settings import ZoneSettings
@@ -25,17 +25,6 @@ from .decorators import logging_helpers
25
25
 
26
26
  @logging_helpers()
27
27
  class Zone:
28
- #
29
- # Zones IDs
30
- #
31
- class IDs(VsslIntEnum):
32
- ZONE_1 = 1
33
- ZONE_2 = 2
34
- ZONE_3 = 3
35
- ZONE_4 = 4
36
- ZONE_5 = 5
37
- ZONE_6 = 6
38
-
39
28
  #
40
29
  # Zone Events
41
30
  #
@@ -49,7 +38,7 @@ class Zone:
49
38
  VOLUME_CHANGE = PREFIX + "volume_change"
50
39
  MUTE_CHANGE = PREFIX + "mute_change"
51
40
 
52
- def __init__(self, vssl_host: "core.Vssl", zone_id: "Zone.IDs", host: str):
41
+ def __init__(self, vssl_host: "core.Vssl", zone_id: ZoneIDs, host: str):
53
42
  self._log_prefix = f"Zone {zone_id}:"
54
43
 
55
44
  self.vssl = vssl_host
@@ -57,7 +46,7 @@ class Zone:
57
46
 
58
47
  # Data / Cache
59
48
  self._host = host
60
- self._id = self.IDs(zone_id)
49
+ self._id = ZoneIDs(zone_id)
61
50
  self._mac_addr = None
62
51
  self._serial = None
63
52
  self._volume = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vsslctrl
3
- Version: 0.1.2.dev1
3
+ Version: 0.1.4.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,43 @@ 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
+ **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
36
 
33
37
  ## TODOs
34
38
 
35
39
  * Test on other models (hardware needed)
36
40
  * Home Assistant integration. In progress, [here](https://github.com/vsslctrl/integration.home-assistant)
37
- * Function scoping to supported feature / models
41
+ * ~~Function scoping to supported feature / models~~ (needs tests)
38
42
  * Better test coverage
39
43
 
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
44
  Basic Usage
46
45
  -----------
47
46
  `vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
48
47
 
49
48
  ```python
50
49
  import asyncio
51
- from vsslctrl import Vssl, Zone
50
+ from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
52
51
 
53
52
  async def main():
54
53
 
55
54
  # Represents a physical VSSL amplifier
56
55
  vssl = Vssl()
57
56
 
57
+ """Optional to init Vssl with a device model
58
+
59
+ vssl = Vssl(DeviceModels.A3X)
60
+
61
+ If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
62
+ """
63
+
58
64
  # 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')
65
+ zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
66
+ zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
67
+ zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
62
68
  #... up to 6 zones
63
69
 
64
70
  # Connect and initiate zones.
@@ -110,7 +116,7 @@ print(zone_name)
110
116
  | ---------------------- | ----------- | ----------- |
111
117
  | `sw_version` | Software version | `str` readonly
112
118
  | `serial` | Serial number | `str` readonly
113
- | `model_zone_qty` | Number of zones the device has | `int` readonly
119
+ | `model` | Device Model | `int` readonly
114
120
  | `reboot()` | Reboot all zones | `func` |
115
121
 
116
122
  ```python
@@ -152,7 +158,7 @@ vssl.settings.power.adaptive = True
152
158
 
153
159
  | Property | Description | Type | Values |
154
160
  | ---------------------- | ----------- | ----------- |----------- |
155
- | `id` | Zone number / ID | `int` readonly | `Zone.IDs`
161
+ | `id` | Zone number / ID | `int` readonly | `ZoneIDs`
156
162
  | `host` | IP address | `str` readonly
157
163
  | `volume` | Volume | `int` | `0...100`
158
164
  | `volume_raise([step=1])` | Raise volume by `step` | `func` | step: `int` `1...100`
@@ -256,20 +262,20 @@ Working on A.3x but offically unsupported in x series amplifiers.
256
262
 
257
263
  | Property | Description | Type | Values |
258
264
  | ---------------------- | ----------- | ----------- |----------- |
259
- | `source` | Zone ID of group master / source | `int` readonly | `Zone.IDs`
265
+ | `source` | Zone ID of group master / source | `int` readonly | `ZoneIDs`
260
266
  | `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`
267
+ | `add_member()` | Add zone to group / create group | `func` | `ZoneIDs`
268
+ | `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
263
269
  | `dissolve()` | Dissolve group / remove all members | `func` |
264
270
  | `leave()` | Leave the group if a member | `func` |
265
271
 
266
272
  ```python
267
273
  """Examples"""
268
274
  # 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.
275
+ zone1.group.add_member(ZoneIDs.ZONE_2)
276
+ # Remove zone 2 from group
271
277
  zone2.group.leave() # or
272
- zone1.group.remove_member(Zone.IDs.ZONE_2)
278
+ zone1.group.remove_member(ZoneIDs.ZONE_2)
273
279
  # If zone 1 is a master, remove all members
274
280
  zone1.group.dissolve()
275
281
  ```
@@ -371,6 +377,53 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
371
377
  zone1.settings.eq.khz1_db = -2
372
378
  ```
373
379
 
380
+ ## Another (Lite) Way
381
+
382
+ 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 ant network tool) on port `50002`.
383
+
384
+ | HEX | Description |
385
+ | ---------------------- | ----------- |
386
+ | `\x10\x05\x03\x0{Zone Number}\xff\x03` | Volume Up
387
+ | `\x10\x05\x03\x0{Zone Number}\xfe\x03` | Volume Down
388
+ | `\x10\x11\x02\x0{Zone Number}\x01` | Mute
389
+ | All commands can be found by looking [here](https://github.com/vsslctrl/vsslctrl/blob/2c43c2f2393b94bc0e062d2ab90144343eca16ef/vsslctrl/api_alpha.py) |
390
+
391
+
392
+ The `volume up` HEX command for `Zone 2` would be `\x10\x05\x03\x02\xff\x03`
393
+
394
+ Now send the raw HEX using Netcat to the device using this syntax:
395
+
396
+ `echo -e "{HEX Command}" | nc {IP Address} 50002`
397
+
398
+ For example to send `volume up` to `Zone 2`:
399
+
400
+ `echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002`
401
+
402
+ Home Assistant `Configuration.yaml` example:
403
+
404
+ ```ymal
405
+ ...
406
+
407
+ shell_command:
408
+ #Zone 1
409
+ vssl_zone_1_volume_up: 'echo -e "\x10\x05\x03\x01\xff\x03" | nc 192.168.1.10 50002'
410
+ vssl_zone_1_volume_down: 'echo -e "\x10\x05\x03\x01\xfe\x03" | nc 192.168.1.10 50002'
411
+ vssl_zone_1_mute: 'echo -e "\x10\x11\x02\x01\x01" | nc 192.168.1.10 50002'
412
+ vssl_zone_1_unmute: 'echo -e "\x10\x11\x02\x01\x00" | nc 192.168.1.10 50002'
413
+ #Zone 2
414
+ vssl_zone_2_volume_up: 'echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002'
415
+ vssl_zone_2_volume_down: 'echo -e "\x10\x05\x03\x02\xfe\x03" | nc 192.168.1.11 50002'
416
+ vssl_zone_2_mute: 'echo -e "\x10\x11\x02\x02\x01" | nc 192.168.1.11 50002'
417
+ vssl_zone_2_unmute: 'echo -e "\x10\x11\x02\x02\x00" | nc 192.168.1.11 50002'
418
+ #Zone 3
419
+ vssl_zone_3_volume_up: 'echo -e "\x10\x05\x03\x03\xff\x03" | nc 192.168.1.12 50002'
420
+ vssl_zone_3_volume_down: 'echo -e "\x10\x05\x03\x03\xfe\x03" | nc 192.168.1.12 50002'
421
+ vssl_zone_3_mute: 'echo -e "\x10\x11\x02\x03\x01" | nc 192.168.1.12 50002'
422
+ vssl_zone_3_unmute: 'echo -e "\x10\x11\x02\x03\x00" | nc 192.168.1.12 50002'
423
+
424
+ ...
425
+ ```
426
+
374
427
  ## Credit
375
428
 
376
429
  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).
@@ -392,4 +445,5 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
392
445
  * REST API / Web App
393
446
  * Save and recall EQ
394
447
  * A.1(x) coverage i.e Bluetooth
448
+ * IR Control
395
449
 
@@ -12,6 +12,7 @@ vsslctrl/api_bravo.py
12
12
  vsslctrl/core.py
13
13
  vsslctrl/data_structure.py
14
14
  vsslctrl/decorators.py
15
+ vsslctrl/device.py
15
16
  vsslctrl/discovery.py
16
17
  vsslctrl/event_bus.py
17
18
  vsslctrl/exceptions.py
@@ -1,4 +0,0 @@
1
- from .core import Vssl
2
- from .zone import Zone
3
-
4
- VSSL_NAME = "VSSL"
File without changes
File without changes