vsslctrl 0.1.9.dev1__py3-none-any.whl → 0.1.10.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vsslctrl/__init__.py CHANGED
@@ -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"
vsslctrl/api_alpha.py CHANGED
@@ -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
  #
vsslctrl/core.py CHANGED
@@ -4,6 +4,7 @@
4
4
  import asyncio
5
5
  from typing import Dict, Union, List
6
6
 
7
+ from . import VSSL_VERSION
7
8
  from .zone import Zone
8
9
  from .exceptions import VsslCtrlException, ZoneError, ZeroConfNotInstalled
9
10
  from .event_bus import EventBus
@@ -93,6 +94,7 @@ class Vssl:
93
94
  raise VsslCtrlException(message)
94
95
 
95
96
  # Output a bit of helpful info
97
+ self._log_info(f"vsslctrl Version: {VSSL_VERSION}")
96
98
  self._log_info(f"Device Model: {self.model.name}")
97
99
  self._log_info(f"Device SW Version: {self.sw_version}")
98
100
  self._log_info(f"Device Serial: {self.serial}")
@@ -196,6 +198,10 @@ class Vssl:
196
198
  #
197
199
  #
198
200
  # THIS WONT WORK FOR ORIGINAL A SERIES! TODO!
201
+ # Maybe we can just use the ID of the zone because 7 will be an A1(x) otherwise
202
+ # we will need to count the zones.
203
+ #
204
+ # Or we just require a model to be passed?
199
205
  #
200
206
  #
201
207
  def _infer_device_model(self, data: Dict[str, int]):
@@ -301,6 +307,14 @@ class Vssl:
301
307
  if zone:
302
308
  zone.api_alpha.request_action_33_device()
303
309
 
310
+ #
311
+ # Factory Reset Device
312
+ #
313
+ def factory_reset(self):
314
+ zone = self.get_connected_zone()
315
+ if zone:
316
+ zone.api_alpha.request_action_2B()
317
+
304
318
  #
305
319
  # Zones Groups. Build a dict of zone according to group membership
306
320
  #
@@ -49,6 +49,16 @@ JSON Structure
49
49
 
50
50
  DO NOT CHANGE - VSSL Defined
51
51
 
52
+ A.1
53
+ {
54
+ "B1Src":"3",
55
+ "B2Src":"3",
56
+ "B1Nm":"OPT",
57
+ "B2Nm":"COAX",
58
+ "dev":"VSSL A.1",
59
+ "ver":"p15265.033.3703"
60
+ }
61
+
52
62
  A.3x
53
63
  {
54
64
  "B1Src": "3",
@@ -112,17 +122,15 @@ DO NOT CHANGE - VSSL Defined
112
122
  IRMskL / IRMskH:
113
123
  These could potentially be related to Infrared (IR) remote control signals. "IRMskL" and "IRMskH" might represent the low and high values of the modulation frequency or pulse width for an infrared signal.
114
124
 
115
- BTSta:
116
- This might represent the Bluetooth status, with "0" indicating that Bluetooth is currently not active or disconnected.
117
-
118
- Crs:
119
- It could stand for "Crossfade" and may represent a setting related to crossfading between audio tracks.
120
-
121
- Fes:
122
- This might stand for "Frequency" or "Filter Effect Setting," representing a parameter related to frequency or filtering effects.
123
-
124
- Drk: ?
125
-
125
+ A.1
126
+ {
127
+ 'IRMskL': '255',
128
+ 'IRMskH': '255',
129
+ 'BTSta': '0',
130
+ 'Crs': '0',
131
+ 'Fes': '0',
132
+ 'Drk': '0'
133
+ }
126
134
 
127
135
  A.3x
128
136
  {
@@ -161,7 +169,7 @@ class DeviceStatusExtendedExtKeys:
161
169
  IR_HIGH = "IRMskH" # Guess - To Confirm
162
170
  IR_LOW = "IRMskL" # Guess - To Confirm
163
171
  BLUETOOTH_STATUS = "BTSta" # Guess - To Confirm
164
- SUBWOOFER_CROSSOVER = "Crs" # Guess - To Confirm
172
+ SUBWOOFER_CROSSOVER = "Crs"
165
173
 
166
174
 
167
175
  """
@@ -169,6 +177,23 @@ JSON Structure
169
177
 
170
178
  DO NOT CHANGE - VSSL Defined
171
179
 
180
+ A.1
181
+ {
182
+ 'id': '7',
183
+ 'ac': '1',
184
+ 'mc': 'XXXXXXXXXXXX',
185
+ 'vol': '20',
186
+ 'mt': '0',
187
+ 'pa': '0',
188
+ 'rm': '0',
189
+ 'ts': '0',
190
+ 'lb': '17',
191
+ 'tp': '1',
192
+ 'wr': '0',
193
+ 'as': '0',
194
+ 'rg': '0'
195
+ }
196
+
172
197
  A.3x
173
198
  {
174
199
  "id": "1",
@@ -234,7 +259,7 @@ class ZoneStatusExtKeys:
234
259
  SERIAL_NUMBER = "mc"
235
260
  VOLUME = "vol"
236
261
  MUTE = "mt"
237
- PARTY_MODE = "pa"
262
+ PARTY_ZONE = "pa"
238
263
  GROUP_INDEX = "rm"
239
264
  TRACK_SOURCE = "lb"
240
265
  DISABLED = "wr"
@@ -245,10 +270,26 @@ JSON Structure
245
270
 
246
271
  DO NOT CHANGE - VSSL Defined
247
272
 
273
+ A.1
274
+ {
275
+ "mono":"0",
276
+ "AiNm":"",
277
+ "eq1":"100",
278
+ "eq2":"97",
279
+ "eq3":"103",
280
+ "eq4":"103",
281
+ "eq5":"103",
282
+ "eq6":"99",
283
+ "eq7":"99",
284
+ "voll":"75",
285
+ "volr":"75",
286
+ "vold":"0"
287
+ }
288
+
248
289
  A.3x
249
290
  {
250
291
  "mono": "0",
251
- "AiNm": "Analog In 1",
292
+ "AiNm": "Analog In",
252
293
  "eq1": "100",
253
294
  "eq2": "100",
254
295
  "eq3": "100",
@@ -264,7 +305,7 @@ A.3x
264
305
  A.3
265
306
  {
266
307
  'mono': '0',
267
- 'AiNm': 'Backup',
308
+ 'AiNm': 'Analog In',
268
309
  'eq1': '106',
269
310
  'eq2': '102',
270
311
  'eq3': '100',
@@ -315,6 +356,22 @@ JSON Structure
315
356
 
316
357
  DO NOT CHANGE - VSSL Defined
317
358
 
359
+ A.1
360
+ {
361
+ "ECO":"0",
362
+ "eqsw":"1",
363
+ "inSrc":"3",
364
+ "SP":"0",
365
+ "BF1":"0",
366
+ "BF2":"0",
367
+ "GRM":"0",
368
+ "GRS":"255",
369
+ "Pwr":"1",
370
+ "Bvr":"7",
371
+ "fxv":"20",
372
+ "AtPwr":"1"
373
+ }
374
+
318
375
  A.3x
319
376
  {
320
377
  "ECO": "0",
@@ -374,8 +431,8 @@ class ZoneRouterStatusExtKeys:
374
431
  EQ_ENABLED = "eqsw"
375
432
  INPUT_SOURCE = "inSrc"
376
433
  SOURCE_PRIORITY = "SP"
377
- ANALOG_OUTPUT_1_FIXED_VOLUME = "BF1"
378
- ANALOG_OUTPUT_2_FIXED_VOLUME = "BF2"
434
+ ANALOG_OUTPUT_1_FIXED_VOLUME = "BF1" # TODO On A3 / A6 is this Bus 1?
435
+ ANALOG_OUTPUT_2_FIXED_VOLUME = "BF2" # TODO On A6 is this Bus 2?
379
436
  ANALOG_OUTPUT_3_FIXED_VOLUME = "BF3"
380
437
  ANALOG_OUTPUT_4_FIXED_VOLUME = "BF4"
381
438
  ANALOG_OUTPUT_5_FIXED_VOLUME = "BF5"
vsslctrl/device.py CHANGED
@@ -9,7 +9,7 @@ A.1x:
9
9
  SUB OUT
10
10
  ANALOG IN 1
11
11
  ANALOG OUT 1
12
- COAXIAL IN (Are Coxial and Optical inputs linked?)
12
+ COAXIAL IN
13
13
  COAXIAL OUT
14
14
  OPTICAL IN
15
15
  OPTICAL OUT
@@ -19,7 +19,7 @@ A1:
19
19
  SUB OUT
20
20
  ANALOG IN 1
21
21
  ANALOG OUT 1
22
- COAXIAL IN (Are Coxial and Optical inputs linked?)
22
+ COAXIAL IN
23
23
  COAXIAL OUT
24
24
  OPTICAL IN
25
25
  OPTICAL OUT
@@ -77,6 +77,8 @@ ANALOG IN 6
77
77
 
78
78
  ---------------------------------
79
79
 
80
+ Following is from Source: https://vssl.gitbook.io/vssl-rest-api/zone-control/set-analog-input-source
81
+
80
82
  Analog Input Source legend: input: inputs specific to model (e.g. A3 does not have zones 4-6 or bus input 2, only A3 has optical input, ...)
81
83
 
82
84
 
@@ -92,8 +94,7 @@ Analog Input Source legend: input: inputs specific to model (e.g. A3 does not ha
92
94
  16 - optical input
93
95
 
94
96
 
95
- Source: https://vssl.gitbook.io/vssl-rest-api/zone-control/set-analog-input-source
96
-
97
+ TODO COAX Input and output!
97
98
 
98
99
  """
99
100
 
@@ -180,7 +181,7 @@ ANALOG_OUTPUT_SOURCES_FOR_A6 = list(AnalogOutput.Sources)
180
181
  class Features(VsslIntEnum):
181
182
  GROUPING = 1000
182
183
  BLUETOOTH = 1001
183
- PARTY_MODE = 1002
184
+ PARTY_ZONE = 1002
184
185
  SUBWOOFER_CROSSOVER = 1003
185
186
 
186
187
 
@@ -247,7 +248,7 @@ class Models(VsslEnum):
247
248
  "zones": THREE_ZONES,
248
249
  "input_sources": INPUT_SOURCES_FOR_A3,
249
250
  "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_A3,
250
- "features": [Features.GROUPING, Features.PARTY_MODE],
251
+ "features": [Features.GROUPING, Features.PARTY_ZONE],
251
252
  }
252
253
  )
253
254
  A6 = Model(
@@ -256,7 +257,7 @@ class Models(VsslEnum):
256
257
  "zones": SIX_ZONES,
257
258
  "input_sources": INPUT_SOURCES_FOR_A6,
258
259
  "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_A6,
259
- "features": [Features.GROUPING, Features.PARTY_MODE],
260
+ "features": [Features.GROUPING, Features.PARTY_ZONE],
260
261
  }
261
262
  )
262
263
 
vsslctrl/group.py CHANGED
@@ -32,8 +32,14 @@ class ZoneGroup(ZoneDataClass):
32
32
  INDEX_CHANGE = PREFIX + "index_change"
33
33
  SOURCE_CHANGE = PREFIX + "source_change"
34
34
  IS_MASTER_CHANGE = PREFIX + "is_master_change"
35
+ IS_PARTY_ZONE_MEMBER_CHANGE = PREFIX + "is_party_zone_member_change"
35
36
 
36
- DEFAULTS = {"index": 0, "source": None, "is_master": False}
37
+ DEFAULTS = {
38
+ "index": 0,
39
+ "source": None,
40
+ "is_master": False,
41
+ "is_party_zone_member": False,
42
+ }
37
43
 
38
44
  def __init__(self, zone: "zone.Zone"):
39
45
  self.zone = zone
@@ -52,9 +58,10 @@ class ZoneGroup(ZoneDataClass):
52
58
  self._index_id = zone.id + 8
53
59
 
54
60
  # index is assigned when a stream is started (see action_32)
55
- self._index = 0
56
- self._source = None
57
- self._is_master = False
61
+ self._index = self.DEFAULTS["index"]
62
+ self._source = self.DEFAULTS["source"]
63
+ self._is_master = self.DEFAULTS["is_master"]
64
+ self._is_party_zone_member = self.DEFAULTS["is_party_zone_member"]
58
65
 
59
66
  #
60
67
  # Group Add Zone
@@ -191,3 +198,22 @@ class ZoneGroup(ZoneDataClass):
191
198
  for zone in self.zone.vssl.zones.values()
192
199
  if zone.group.index == self.index and zone.group.is_member
193
200
  ]
201
+
202
+ #
203
+ # Party Mode - Is this zone a party mode member
204
+ #
205
+ @property
206
+ def is_party_zone_member(self):
207
+ return bool(self._is_party_zone_member)
208
+
209
+ @is_party_zone_member.setter
210
+ def is_party_zone_member(self, state: int):
211
+ # Check this device supports party mode
212
+ if not self.zone.vssl.model.supports_feature(DeviceFeatures.PARTY_ZONE):
213
+ self.zone._log_error(f"VSSL {self.zone.vssl.model.name} doesnt party zone")
214
+ return
215
+
216
+ self.zone.api_alpha.request_action_0C(state)
217
+
218
+ def is_party_zone_member_toggle(self):
219
+ self.is_party_zone_member = False if self.is_party_zone_member else True
vsslctrl/io.py CHANGED
@@ -25,8 +25,8 @@ class InputRouter(ZoneDataClass):
25
25
  #
26
26
  class Sources(VsslIntEnum):
27
27
  STREAM = 0
28
- BUS_IN_1 = 1 # TODO bus or party mode of original A series
29
- BUS_IN_2 = 2 # TODO bus or party mode of original A series
28
+ BUS_IN_1 = 1
29
+ BUS_IN_2 = 2
30
30
  ANALOG_IN_1 = 3
31
31
  ANALOG_IN_2 = 4
32
32
  ANALOG_IN_3 = 5
@@ -112,7 +112,13 @@ class AnalogOutput(ZoneDataClass):
112
112
  Should this be on the VSSL or Zone? For now its on the zone, because the zone will
113
113
  receive feedback for the corrosponding analog output id
114
114
 
115
- Zones will be determined by source Input Priority
115
+ Zones will be determined by source Input Priority @see InputRouter class
116
+
117
+ TODO: Support for Bus 1 & 2 - Bus 1 and 2 outs can have fixed volume also.
118
+ Since the BUS zones dont have a dedicated zone, maybe we should move these
119
+ commands to the VSSL device control class instead of the zone class?
120
+
121
+ TODO: A1 input and output mappings!
116
122
  """
117
123
 
118
124
  #
@@ -122,8 +128,8 @@ class AnalogOutput(ZoneDataClass):
122
128
  #
123
129
  class Sources(VsslIntEnum):
124
130
  OFF = 0 # Disconnected / Off / No Output
125
- BUS_IN_1 = 1 # TODO bus or party mode of original A series
126
- BUS_IN_2 = 2 # TODO bus or party mode of original A series
131
+ BUS_IN_1 = 1 # TODO bus or party mode of original A series?
132
+ BUS_IN_2 = 2 # TODO bus or party mode of original A series?
127
133
  ZONE_1 = 3
128
134
  ZONE_2 = 4
129
135
  ZONE_3 = 5
@@ -149,10 +155,10 @@ class AnalogOutput(ZoneDataClass):
149
155
  self.zone = zone
150
156
 
151
157
  self._is_fixed_volume = self.DEFAULTS["is_fixed_volume"]
152
- self._source = self.Sources(zone.id + 2)
158
+ self._source = self.DEFAULTS["source"]
153
159
 
154
160
  #
155
- # Analog Output Fix Volume. Output wont respond to volume control
161
+ # Analog Output Fix Volume. i.e output wont respond to volume control
156
162
  #
157
163
  @property
158
164
  def is_fixed_volume(self):
vsslctrl/settings.py CHANGED
@@ -690,8 +690,8 @@ class EQSettings(ZoneDataClass):
690
690
 
691
691
  # Not in io.py to prevent circ import in device.py
692
692
  class SubwooferSettings(ZoneDataClass):
693
- MIN_VALUE = 0
694
- MAX_VALUE = 1000
693
+ MIN_VALUE = 50
694
+ MAX_VALUE = 200
695
695
 
696
696
  #
697
697
  # Subwoofer events
@@ -703,6 +703,10 @@ class SubwooferSettings(ZoneDataClass):
703
703
  #
704
704
  # Defaults
705
705
  #
706
+ # 0 is full range / off
707
+ #
708
+ # The crossover is adjustable from 50-200Hz.
709
+ #
706
710
  DEFAULTS = {"crossover": 0}
707
711
 
708
712
  def __init__(self, zone: "zone.Zone"):
@@ -713,7 +717,11 @@ class SubwooferSettings(ZoneDataClass):
713
717
  #
714
718
  # Clamp between min and max
715
719
  #
716
- def _clamp_crossover(self, value: int = 100):
720
+ def _clamp_crossover(self, value: int = 0):
721
+ # Allow 0 to enable full range setting
722
+ if value < self.MIN_VALUE:
723
+ return 0
724
+
717
725
  return int(max(self.MIN_VALUE, min(value, self.MAX_VALUE)))
718
726
 
719
727
  #
@@ -732,11 +740,8 @@ class SubwooferSettings(ZoneDataClass):
732
740
  f"VSSL {self.zone.vssl.model.name} does not have a subwoofer output"
733
741
  )
734
742
  return
735
- #
736
- # TODO
737
- #
738
- self.zone._log_info(f"TODO - crossover set function")
739
- # self.zone.api_alpha.request_action_crossover(freq)
743
+
744
+ self.zone.api_alpha.request_action_57(freq)
740
745
 
741
746
  def _set_crossover(self, freq: int):
742
747
  freq = self._clamp_crossover(freq)
@@ -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
@@ -0,0 +1,23 @@
1
+ vsslctrl/__init__.py,sha256=Pi0MgplOWvu3EpicZcmY05ctjtsmj-Stvv9GQZVsJW0,174
2
+ vsslctrl/api_alpha.py,sha256=I3BcyphEHtW89vN363W7hiGHiPhHnQFO6krmxtMwGQc,35504
3
+ vsslctrl/api_base.py,sha256=a80CRtUkYK2OwXQOL8bP-YOvt4YE-ALd-VONza_gjK8,9623
4
+ vsslctrl/api_bravo.py,sha256=pchZN16KIvdSrqkI1WU9PA9IEmxkyyP-XifxpRC0jDE,12283
5
+ vsslctrl/core.py,sha256=uUNb8bU2JRZxDbzWgwrblXjKHYdeIWbuogSp9qQX3U8,9555
6
+ vsslctrl/data_structure.py,sha256=Doxyl5S6ecdAKk1Z4UO1BlCw2kAntUtU4RBVXhW9ZN0,10322
7
+ vsslctrl/decorators.py,sha256=lG8cYSSA-fNvdcp_90lv58tj50bF6iPoigLKcucWSSI,1707
8
+ vsslctrl/device.py,sha256=zJg51SiOv8IRyxHRXx7SrYXeyOGEeZY3id7NL3fTuTw,6070
9
+ vsslctrl/discovery.py,sha256=3z7z6YQUtvjFvRlQOZXIu6Wd4M896rMbi8SUkTub3AM,5568
10
+ vsslctrl/event_bus.py,sha256=9liAEJnZfthJwy5O-qc3pDIWRBv5AcV19PjMCwH_niU,5014
11
+ vsslctrl/exceptions.py,sha256=EEfNlu--9PMIidj3V3ZB8c7p3My8siqKw7YIOjPFaqE,419
12
+ vsslctrl/group.py,sha256=DJRD3pLha5tvpN_RTO8Zg311TaNWQ_O4p5bJjZ47aWE,6311
13
+ vsslctrl/io.py,sha256=bucsUQXUAlXx8D_s_Sa20eTamT_FiR1cKwMX4Yl4ySM,6902
14
+ vsslctrl/settings.py,sha256=_VtZLTYXdo2otyqs4iruCaOwclnoMafpsvPS1UJGXA4,19083
15
+ vsslctrl/track.py,sha256=_lQNHYe8ZBm3v1lehFLjZruF5-Rz8PGWuQN-zISBgwc,10136
16
+ vsslctrl/transport.py,sha256=XJFy0kl9y7hm3YV4_9ElBrq3u6--_k73UKJqKdO6Rqg,6993
17
+ vsslctrl/utils.py,sha256=FWOMHM_sl1Cp1xab-V6b-A5B7qRmDN67hMMxV4ReWCk,2087
18
+ vsslctrl/zone.py,sha256=teSk8oE3LKteidRnuVK3koUOqtN-90LclEx0-T4FhAE,12978
19
+ vsslctrl-0.1.10.dev1.dist-info/LICENSE,sha256=67Ekj8PIxjEvkPd7L3GYJ8dyvn1EC4sPM3q3FKtpN6w,1065
20
+ vsslctrl-0.1.10.dev1.dist-info/METADATA,sha256=VJzk7TXVa5INkaz-0oZyzYUlsQHNeSZmHu6Vf5Ds-b8,18817
21
+ vsslctrl-0.1.10.dev1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
22
+ vsslctrl-0.1.10.dev1.dist-info/top_level.txt,sha256=MRycO_RKageNX27UZ2bLJDMlfeWTQzUZlsd3hS-0u3Y,9
23
+ vsslctrl-0.1.10.dev1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,23 +0,0 @@
1
- vsslctrl/__init__.py,sha256=W7cljoE7L3DukAVr_TEqkxFycYrUbJiqaHrOlbPkmKU,145
2
- vsslctrl/api_alpha.py,sha256=UxsMKOFK4Uit3QoF96Wa3VE1WdbCq7FOJjt-3fewAe0,33962
3
- vsslctrl/api_base.py,sha256=a80CRtUkYK2OwXQOL8bP-YOvt4YE-ALd-VONza_gjK8,9623
4
- vsslctrl/api_bravo.py,sha256=pchZN16KIvdSrqkI1WU9PA9IEmxkyyP-XifxpRC0jDE,12283
5
- vsslctrl/core.py,sha256=G6XSH2_sxVaB6AfR6Btt8SMqiihlBQ6n_UZB9lsO7fw,9114
6
- vsslctrl/data_structure.py,sha256=i6xG72kQqFDSoEhI307Iw9QXTquMNMc4qMUGOK0MEbw,9793
7
- vsslctrl/decorators.py,sha256=lG8cYSSA-fNvdcp_90lv58tj50bF6iPoigLKcucWSSI,1707
8
- vsslctrl/device.py,sha256=M6fcd2ggE-ConHoENpQ3WtGQzmSyok9t-8Kt5pd-r6U,6104
9
- vsslctrl/discovery.py,sha256=3z7z6YQUtvjFvRlQOZXIu6Wd4M896rMbi8SUkTub3AM,5568
10
- vsslctrl/event_bus.py,sha256=9liAEJnZfthJwy5O-qc3pDIWRBv5AcV19PjMCwH_niU,5014
11
- vsslctrl/exceptions.py,sha256=EEfNlu--9PMIidj3V3ZB8c7p3My8siqKw7YIOjPFaqE,419
12
- vsslctrl/group.py,sha256=B7lmJb6DIwXGnMyTKsp-JfRWVCVRFQYzlvS71tutfx4,5368
13
- vsslctrl/io.py,sha256=RZCLxa0RAnyhRdDTM7IFpLcpYeQfV90DzVEJdNjR6lQ,6684
14
- vsslctrl/settings.py,sha256=ZUa_ixohGzowj6mhGlCcVAr_ZfsbpU_N5jK-7pEA5Jc,18997
15
- vsslctrl/track.py,sha256=_lQNHYe8ZBm3v1lehFLjZruF5-Rz8PGWuQN-zISBgwc,10136
16
- vsslctrl/transport.py,sha256=XJFy0kl9y7hm3YV4_9ElBrq3u6--_k73UKJqKdO6Rqg,6993
17
- vsslctrl/utils.py,sha256=FWOMHM_sl1Cp1xab-V6b-A5B7qRmDN67hMMxV4ReWCk,2087
18
- vsslctrl/zone.py,sha256=teSk8oE3LKteidRnuVK3koUOqtN-90LclEx0-T4FhAE,12978
19
- vsslctrl-0.1.9.dev1.dist-info/LICENSE,sha256=67Ekj8PIxjEvkPd7L3GYJ8dyvn1EC4sPM3q3FKtpN6w,1065
20
- vsslctrl-0.1.9.dev1.dist-info/METADATA,sha256=fKQI1brfCRDqoXlXfPNB_22heY6oQC-fxjADf02JYro,16990
21
- vsslctrl-0.1.9.dev1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
22
- vsslctrl-0.1.9.dev1.dist-info/top_level.txt,sha256=MRycO_RKageNX27UZ2bLJDMlfeWTQzUZlsd3hS-0u3Y,9
23
- vsslctrl-0.1.9.dev1.dist-info/RECORD,,