vsslctrl 0.1.6.dev1__tar.gz → 0.1.8.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.6.dev1 → vsslctrl-0.1.8.dev1}/PKG-INFO +8 -1
  2. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/README.md +7 -0
  3. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/pyproject.toml +1 -1
  4. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/api_alpha.py +117 -52
  5. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/core.py +5 -0
  6. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/data_structure.py +129 -8
  7. vsslctrl-0.1.8.dev1/vsslctrl/device.py +261 -0
  8. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/discovery.py +1 -1
  9. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/io.py +18 -6
  10. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/settings.py +94 -4
  11. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/zone.py +4 -2
  12. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl.egg-info/PKG-INFO +8 -1
  13. vsslctrl-0.1.6.dev1/vsslctrl/device.py +0 -115
  14. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/LICENSE +0 -0
  15. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/setup.cfg +0 -0
  16. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/tests/test_event_bus.py +0 -0
  17. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/tests/test_integration.py +0 -0
  18. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/tests/test_settings.py +0 -0
  19. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/tests/test_zone.py +0 -0
  20. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/__init__.py +0 -0
  21. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/api_base.py +0 -0
  22. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/api_bravo.py +0 -0
  23. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/decorators.py +0 -0
  24. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/event_bus.py +0 -0
  25. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/exceptions.py +0 -0
  26. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/group.py +0 -0
  27. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/track.py +0 -0
  28. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/transport.py +0 -0
  29. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl/utils.py +0 -0
  30. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl.egg-info/SOURCES.txt +0 -0
  31. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl.egg-info/dependency_links.txt +0 -0
  32. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.dev1}/vsslctrl.egg-info/requires.txt +0 -0
  33. {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.8.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.6.dev1
3
+ Version: 0.1.8.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
@@ -247,6 +247,13 @@ zone1.transport.state = ZoneTransport.States.PAUSE
247
247
  | `source` | Change input source | `int` | `InputRouter.Sources`
248
248
  | `priority` | Change input priority. Stream or analog in higher priority | `int` | `InputRouter.Priorities`
249
249
 
250
+ ### Priority Order:
251
+
252
+ | `InputRouter.Priorities` | Priority Order |
253
+ | ---------------------- | ----------- |
254
+ | **`STREAM`** | <ol><li>Stream</li><li>Party Zone</li><li>Bus 1 In</li><li>Bus 2 In</li><li>Analog Input</li></ol>
255
+ | **`LOCAL`** | <ol><li>Bus 1 In</li><li>Bus 2 In</li><li>Analog Input</li><li>Stream</li><li>Party Zone</li></ol>
256
+
250
257
  ```python
251
258
  """Example"""
252
259
  # Change zone 1 to listen to analog input 4
@@ -226,6 +226,13 @@ zone1.transport.state = ZoneTransport.States.PAUSE
226
226
  | `source` | Change input source | `int` | `InputRouter.Sources`
227
227
  | `priority` | Change input priority. Stream or analog in higher priority | `int` | `InputRouter.Priorities`
228
228
 
229
+ ### Priority Order:
230
+
231
+ | `InputRouter.Priorities` | Priority Order |
232
+ | ---------------------- | ----------- |
233
+ | **`STREAM`** | <ol><li>Stream</li><li>Party Zone</li><li>Bus 1 In</li><li>Bus 2 In</li><li>Analog Input</li></ol>
234
+ | **`LOCAL`** | <ol><li>Bus 1 In</li><li>Bus 2 In</li><li>Analog Input</li><li>Stream</li><li>Party Zone</li></ol>
235
+
229
236
  ```python
230
237
  """Example"""
231
238
  # Change zone 1 to listen to analog input 4
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vsslctrl"
7
- version = "0.1.6.dev1"
7
+ version = "0.1.8.dev1"
8
8
  description = "Package for controlling VSSL's range of streaming amplifiers"
9
9
  authors = [
10
10
  { name="vsslctrl", email="vsslcontrolled@proton.me" },
@@ -16,6 +16,7 @@ from .data_structure import (
16
16
  ZoneEQStatusExtKeys,
17
17
  ZoneRouterStatusExtKeys,
18
18
  DeviceStatusExtKeys,
19
+ DeviceStatusExtendedExtKeys,
19
20
  )
20
21
 
21
22
 
@@ -94,10 +95,10 @@ class APIAlpha(APIBase):
94
95
 
95
96
  #
96
97
  # 00 [0] - 0B [11]
97
- # Status BT (Bluetooth)
98
+ # Status device extended
98
99
  #
99
100
  def request_action_00_0B(self):
100
- self._log_debug("Requesting status bluetooth")
101
+ self._log_debug("Requesting device status extended")
101
102
  self.send(bytearray([16, 0, 1, 11])) # HEX: 1000010B
102
103
 
103
104
  #
@@ -549,12 +550,22 @@ class APIAlpha(APIBase):
549
550
  except Exception as error:
550
551
  self._log_error(f"Couldnt parse JSON: {error} | {hexl}")
551
552
 
552
- #
553
- # 00_00
554
- # Device Status 00
555
- #
556
- # {'B1Src': '3', 'B2Src': '4', 'B3Src': '5', 'B1Nm': '', 'B2Nm': 'Optical In', 'dev': 'Device Name', 'ver': 'p15305.016.3701'}
557
- #
553
+ """
554
+ 00_00
555
+ Device Status 00
556
+
557
+ A.3x:
558
+ {
559
+ "B1Src": "3",
560
+ "B2Src": "4",
561
+ "B3Src": "5",
562
+ "B1Nm": "",
563
+ "B2Nm": "Optical In",
564
+ "dev": "Device Name",
565
+ "ver": "p15305.016.3701"
566
+ }
567
+ """
568
+
558
569
  def response_action_00_00(self, metadata: list):
559
570
  self._log_debug(f"Received 00 Status: {metadata}")
560
571
 
@@ -588,13 +599,32 @@ class APIAlpha(APIBase):
588
599
  "sw_version", metadata[DeviceStatusExtKeys.SW_VERSION].strip()
589
600
  )
590
601
 
591
- #
592
- # 00_08
593
- # Zone Status 08
594
- #
595
- # {'id': '1', 'ac': '0', 'mc': 'XXXXXXXXXXXX', 'vol': '20', 'mt': '0', 'pa': '0', 'rm': '0', 'ts': '14',
596
- # 'alex': '14', 'nmd': '0', 'ird': '14', 'lb': '24', 'tp': '13', 'wr': '0', 'as': '0', 'rg': '0'}
597
- #
602
+ """
603
+ 00_08
604
+ Zone Status 08
605
+
606
+ A.3x:
607
+ {
608
+ "id": "1",
609
+ "ac": "0",
610
+ "mc": "XXXXXXXXXXXX",
611
+ "vol": "20",
612
+ "mt": "0",
613
+ "pa": "0",
614
+ "rm": "0",
615
+ "ts": "14",
616
+ "alex": "14",
617
+ "nmd": "0",
618
+ "ird": "14",
619
+ "lb": "24",
620
+ "tp": "13",
621
+ "wr": "0",
622
+ "as": "0",
623
+ "rg": "0"
624
+ }
625
+
626
+ """
627
+
598
628
  def response_action_00_08(self, metadata: list):
599
629
  self._log_debug(f"Received 08 Status: {metadata}")
600
630
 
@@ -652,13 +682,27 @@ class APIAlpha(APIBase):
652
682
  "disabled", bool(int(metadata[ZoneStatusExtKeys.DISABLED]))
653
683
  )
654
684
 
655
- #
656
- # 00_09
657
- # EQ Status
658
- #
659
- # {'mono': '0', 'AiNm': 'Analog In 1', 'eq1': '100', 'eq2': '100', 'eq3': '100', 'eq4': '100',
660
- # 'eq5': '100', 'eq6': '100', 'eq7': '100', 'voll': '75', 'volr': '75', 'vold': '0'}
661
- #
685
+ """
686
+ 00_09
687
+ EQ Status
688
+
689
+ A.3x:
690
+ {
691
+ "mono": "0",
692
+ "AiNm": "Analog In 1",
693
+ "eq1": "100",
694
+ "eq2": "100",
695
+ "eq3": "100",
696
+ "eq4": "100",
697
+ "eq5": "100",
698
+ "eq6": "100",
699
+ "eq7": "100",
700
+ "voll": "75",
701
+ "volr": "75",
702
+ "vold": "0"
703
+ }
704
+ """
705
+
662
706
  def response_action_00_09(self, metadata: list):
663
707
  self._log_debug(f"Received 09 Status: {metadata}")
664
708
 
@@ -677,13 +721,28 @@ class APIAlpha(APIBase):
677
721
  self.zone.settings.eq._map_response_dict(metadata)
678
722
  self.zone.settings.volume._map_response_dict(metadata)
679
723
 
680
- #
681
- # 00_0A
682
- # System Status 0A (Output / Amp)
683
- #
684
- # {'ECO': '0', 'eqsw': '1', 'inSrc': '0', 'SP': '0', 'BF1': '0', 'BF2': '0', 'BF3': '0',
685
- # 'GRM': '0', 'GRS': '255', 'Pwr': '0', 'Bvr': '1', 'fxv': '24', 'AtPwr': '1'}
686
- #
724
+ """
725
+ 00_0A
726
+ System Status 0A (Output / Amp)
727
+
728
+ A.3x:
729
+ {
730
+ "ECO": "0",
731
+ "eqsw": "1",
732
+ "inSrc": "0",
733
+ "SP": "0",
734
+ "BF1": "0",
735
+ "BF2": "0",
736
+ "BF3": "0",
737
+ "GRM": "0",
738
+ "GRS": "255",
739
+ "Pwr": "0",
740
+ "Bvr": "1",
741
+ "fxv": "24",
742
+ "AtPwr": "1"
743
+ }
744
+ """
745
+
687
746
  def response_action_00_0A(self, metadata: list):
688
747
  self._log_debug(f"Received 0A Status: {metadata}")
689
748
 
@@ -744,30 +803,36 @@ class APIAlpha(APIBase):
744
803
  "adaptive", bool(int(metadata[ZoneRouterStatusExtKeys.ADAPTIVE_POWER]))
745
804
  )
746
805
 
747
- #
748
- # 00_0B
749
- # System Status 0B (Not Sure)
750
- #
751
- # {'IRMskL': '241', 'IRMskH': '255', 'BTSta': '0', 'Crs': '0', 'Fes': '0', 'Drk': '0'}
752
- #
753
- #
754
- # IRMskL / IRMskH:
755
- # 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.
756
- #
757
- # BTSta:
758
- # This might represent the Bluetooth status, with "0" indicating that Bluetooth is currently not active or disconnected.
759
- #
760
- # Crs:
761
- # It could stand for "Crossfade" and may represent a setting related to crossfading between audio tracks.
762
- #
763
- # Fes:
764
- # This might stand for "Frequency" or "Filter Effect Setting," representing a parameter related to frequency or filtering effects.
765
- #
766
- # Drk: ?
767
- #
806
+ """
807
+ 00_0B
808
+ Device Status Extended 0B
809
+
810
+ A.3x:
811
+ {
812
+ 'IRMskL': '241',
813
+ 'IRMskH': '255',
814
+ 'BTSta': '0',
815
+ 'Crs': '0',
816
+ 'Fes': '0',
817
+ 'Drk': '0'
818
+ }
819
+ """
820
+
768
821
  def response_action_00_0B(self, metadata: list):
769
822
  self._log_debug(f"Received 0B Status: {metadata}")
770
- pass
823
+
824
+ # Bluetooth
825
+ if DeviceStatusExtendedExtKeys.BLUETOOTH_STATUS in metadata:
826
+ self.vssl.settings._set_property(
827
+ "bluetooth", int(metadata[DeviceStatusExtendedExtKeys.BLUETOOTH_STATUS])
828
+ )
829
+
830
+ # Subwoofer Crossover
831
+ if DeviceStatusExtendedExtKeys.SUBWOOFER_CROSSOVER in metadata:
832
+ self.zone.settings.subwoofer._set_property(
833
+ "crossover",
834
+ int(metadata[DeviceStatusExtendedExtKeys.SUBWOOFER_CROSSOVER]),
835
+ )
771
836
 
772
837
  #
773
838
  # 2A [42]
@@ -927,7 +992,7 @@ class APIAlpha(APIBase):
927
992
  # Keep Alive
928
993
  #
929
994
  def response_action_17(self, hexl: list, response: bytes):
930
- self._log_debug(f"Z{self.zone.id} Alpha - Received keep alive: {response}")
995
+ self._log_debug(f"Received keep alive: {response}")
931
996
  # TODO
932
997
  pass
933
998
 
@@ -93,6 +93,11 @@ class Vssl:
93
93
  await first_zone.disconnect()
94
94
  raise VsslCtrlException(message)
95
95
 
96
+ # Output a bit of helpful info
97
+ self._log_info(f"Device Model: {self.model.name}")
98
+ self._log_info(f"Device SW Version: {self.sw_version}")
99
+ self._log_info(f"Device Serial: {self.serial}")
100
+
96
101
  # Now we can init the rest of the zones
97
102
  initialisations = [zone.initialise() for zone in zones_to_init.values()]
98
103
  await asyncio.gather(*initialisations)
@@ -41,6 +41,7 @@ class ZoneIDs(VsslIntEnum):
41
41
  ZONE_4 = 4
42
42
  ZONE_5 = 5
43
43
  ZONE_6 = 6
44
+ A1 = 7 # A.1 and A.1x
44
45
 
45
46
 
46
47
  """
@@ -48,7 +49,7 @@ JSON Structure
48
49
 
49
50
  DO NOT CHANGE - VSSL Defined
50
51
 
51
- A3.x
52
+ A.3x
52
53
  {
53
54
  "B1Src": "3",
54
55
  "B2Src": "4",
@@ -59,7 +60,17 @@ A3.x
59
60
  "ver": "p15305.016.3701"
60
61
  }
61
62
 
62
- A6.x
63
+ A.3
64
+ {
65
+ 'B1Src': '1',
66
+ 'B2Src': '2',
67
+ 'B1Nm': '',
68
+ 'B2Nm': '',
69
+ 'dev': 'VSSL A.3',
70
+ 'ver': '12013 .141.3703'
71
+ }
72
+
73
+ A.6x
63
74
  {
64
75
  "B1Src": "3",
65
76
  "B2Src": "4",
@@ -97,7 +108,68 @@ JSON Structure
97
108
 
98
109
  DO NOT CHANGE - VSSL Defined
99
110
 
100
- A3.x
111
+
112
+ IRMskL / IRMskH:
113
+ 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
+
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
+
126
+
127
+ A.3x
128
+ {
129
+ 'IRMskL': '241',
130
+ 'IRMskH': '255',
131
+ 'BTSta': '0',
132
+ 'Crs': '0',
133
+ 'Fes': '0',
134
+ 'Drk': '0'
135
+ }
136
+
137
+ A.3
138
+ {
139
+ 'IRMskL': '255',
140
+ 'IRMskH': '255',
141
+ 'BTSta': '0',
142
+ 'Crs': '0',
143
+ 'Fes': '0',
144
+ 'Drk': '0'
145
+ }
146
+
147
+ A.6x
148
+ {
149
+ "IRMskL": "255",
150
+ "IRMskH": "255",
151
+ "BTSta": "0",
152
+ "Crs": "0",
153
+ "Fes": "0",
154
+ "Drk": "0"
155
+ '
156
+
157
+ """
158
+
159
+
160
+ class DeviceStatusExtendedExtKeys:
161
+ IR_HIGH = "IRMskH" # Guess - To Confirm
162
+ IR_LOW = "IRMskL" # Guess - To Confirm
163
+ BLUETOOTH_STATUS = "BTSta" # Guess - To Confirm
164
+ SUBWOOFER_CROSSOVER = "Crs" # Guess - To Confirm
165
+
166
+
167
+ """
168
+ JSON Structure
169
+
170
+ DO NOT CHANGE - VSSL Defined
171
+
172
+ A.3x
101
173
  {
102
174
  "id": "1",
103
175
  "ac": "0",
@@ -117,7 +189,24 @@ A3.x
117
189
  "rg": "0"
118
190
  }
119
191
 
120
- A6.x
192
+ A.3
193
+ {
194
+ 'id': '3',
195
+ 'ac': '0',
196
+ 'mc': 'XXXXXXXXXXXX',
197
+ 'vol': '27',
198
+ 'mt': '0',
199
+ 'pa': '1',
200
+ 'rm': '0',
201
+ 'ts': '0',
202
+ 'lb': '17',
203
+ 'tp': '3',
204
+ 'wr': '0',
205
+ 'as': '0',
206
+ 'rg': '0'
207
+ }
208
+
209
+ A.6x
121
210
  {
122
211
  "id": "1",
123
212
  "ac": "0",
@@ -156,7 +245,7 @@ JSON Structure
156
245
 
157
246
  DO NOT CHANGE - VSSL Defined
158
247
 
159
- A3.x
248
+ A.3x
160
249
  {
161
250
  "mono": "0",
162
251
  "AiNm": "Analog In 1",
@@ -172,7 +261,23 @@ A3.x
172
261
  "vold": "0"
173
262
  }
174
263
 
175
- A6.x
264
+ A.3
265
+ {
266
+ 'mono': '0',
267
+ 'AiNm': 'Backup',
268
+ 'eq1': '106',
269
+ 'eq2': '102',
270
+ 'eq3': '100',
271
+ 'eq4': '100',
272
+ 'eq5': '100',
273
+ 'eq6': '101',
274
+ 'eq7': '105',
275
+ 'voll': '75',
276
+ 'volr': '75',
277
+ 'vold': '27'
278
+ }
279
+
280
+ A.6x
176
281
  {
177
282
  "mono": "0",
178
283
  "AiNm": "",
@@ -210,7 +315,7 @@ JSON Structure
210
315
 
211
316
  DO NOT CHANGE - VSSL Defined
212
317
 
213
- A3.x
318
+ A.3x
214
319
  {
215
320
  "ECO": "0",
216
321
  "eqsw": "1",
@@ -227,7 +332,23 @@ A3.x
227
332
  "AtPwr": "1"
228
333
  }
229
334
 
230
- A6.x
335
+ A.3
336
+ {
337
+ "ECO": "0",
338
+ "eqsw": "1",
339
+ "inSrc": "0",
340
+ "SP": "0",
341
+ "BF1": "0",
342
+ "BF2": "0",
343
+ "GRM": "0",
344
+ "GRS": "255",
345
+ "Pwr": "0",
346
+ "Bvr": "15",
347
+ "fxv": "25",
348
+ "AtPwr": "1"
349
+ }
350
+
351
+ A.6x
231
352
  {
232
353
  "ECO": "0",
233
354
  "eqsw": "1",
@@ -0,0 +1,261 @@
1
+ from .data_structure import VsslEnum, VsslIntEnum, ZoneIDs
2
+ from .io import AnalogOutput, InputRouter
3
+
4
+
5
+ """
6
+ Model Physical IOs, sourced from the product manuals
7
+
8
+ A.1x:
9
+ SUB OUT
10
+ ANALOG IN 1
11
+ ANALOG OUT 1
12
+ COAXIAL IN (Are Coxial and Optical inputs linked?)
13
+ COAXIAL OUT
14
+ OPTICAL IN
15
+ OPTICAL OUT
16
+ IR REMOTE
17
+
18
+ A1:
19
+ SUB OUT
20
+ ANALOG IN 1
21
+ ANALOG OUT 1
22
+ COAXIAL IN (Are Coxial and Optical inputs linked?)
23
+ COAXIAL OUT
24
+ OPTICAL IN
25
+ OPTICAL OUT
26
+ IR REMOTE
27
+
28
+
29
+
30
+ A.3x:
31
+ ANALOG IN 1
32
+ ANALOG IN 2
33
+ ANALOG IN 3
34
+ ANALOG OUT 1
35
+ ANALOG OUT 2
36
+ ANALOG OUT 3
37
+ OPTICAL IN
38
+ IR REMOTE
39
+
40
+ A3:
41
+ BUS 1 IN
42
+ BUS 1 OUT
43
+ ANALOG IN 1
44
+ ANALOG IN 2
45
+ ANALOG IN 3
46
+ OPTICAL IN
47
+
48
+
49
+
50
+ A.6x:
51
+ ANALOG IN 1
52
+ ANALOG IN 2
53
+ ANALOG IN 3
54
+ ANALOG IN 4
55
+ ANALOG IN 5
56
+ ANALOG IN 6
57
+ ANALOG OUT 1
58
+ ANALOG OUT 2
59
+ ANALOG OUT 3
60
+ ANALOG OUT 4
61
+ ANALOG OUT 5
62
+ ANALOG OUT 6
63
+ OPTICAL IN
64
+ IR REMOTE
65
+
66
+ A6:
67
+ BUS 1 IN
68
+ BUS 1 OUT
69
+ BUS 2 IN
70
+ BUS 2 OUT
71
+ ANALOG IN 1
72
+ ANALOG IN 2
73
+ ANALOG IN 3
74
+ ANALOG IN 4
75
+ ANALOG IN 5
76
+ ANALOG IN 6
77
+
78
+ ---------------------------------
79
+
80
+ 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
+
82
+
83
+ 0 - none
84
+ 1 - bus input 1
85
+ 2 - bus input 2
86
+ 3 - zone 1 local input
87
+ 4 - zone 2 local input
88
+ 5 - zone 3 local input
89
+ 6 - zone 4 local input
90
+ 7 - zone 5 local input
91
+ 8 - zone 6 local input
92
+ 16 - optical input
93
+
94
+
95
+ Source: https://vssl.gitbook.io/vssl-rest-api/zone-control/set-analog-input-source
96
+
97
+
98
+ """
99
+
100
+
101
+ """
102
+ Zones
103
+
104
+
105
+ """
106
+ SINGLE_ZONE = [ZoneIDs.A1]
107
+ THREE_ZONES = [ZoneIDs.ZONE_1, ZoneIDs.ZONE_2, ZoneIDs.ZONE_3]
108
+ SIX_ZONES = THREE_ZONES + [ZoneIDs.ZONE_4, ZoneIDs.ZONE_5, ZoneIDs.ZONE_6]
109
+
110
+ """
111
+ Input Sources
112
+
113
+
114
+ """
115
+
116
+ # A.1 & A.1x
117
+ INPUT_SOURCES_FOR_1_ZONE_DEVICE = [
118
+ InputRouter.Sources.STREAM,
119
+ InputRouter.Sources.ANALOG_IN_1,
120
+ InputRouter.Sources.OPTICAL_IN,
121
+ ]
122
+
123
+ # A.3x
124
+ INPUT_SOURCES_FOR_3_ZONE_DEVICE = INPUT_SOURCES_FOR_1_ZONE_DEVICE + [
125
+ InputRouter.Sources.ANALOG_IN_2,
126
+ InputRouter.Sources.ANALOG_IN_3,
127
+ ]
128
+
129
+ # A.3
130
+ INPUT_SOURCES_FOR_A3 = INPUT_SOURCES_FOR_3_ZONE_DEVICE + [InputRouter.Sources.BUS_IN_1]
131
+
132
+ # A.6x
133
+ INPUT_SOURCES_FOR_6_ZONE_DEVICE = INPUT_SOURCES_FOR_3_ZONE_DEVICE + [
134
+ InputRouter.Sources.ANALOG_IN_4,
135
+ InputRouter.Sources.ANALOG_IN_5,
136
+ InputRouter.Sources.ANALOG_IN_6,
137
+ ]
138
+
139
+ # A.6
140
+ INPUT_SOURCES_FOR_A6 = [
141
+ source for source in InputRouter.Sources if source != InputRouter.Sources.OPTICAL_IN
142
+ ]
143
+
144
+
145
+ """
146
+ Analog Output Sources
147
+
148
+
149
+ """
150
+
151
+ # A.1 & A.1x
152
+ ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE = [
153
+ AnalogOutput.Sources.OFF,
154
+ AnalogOutput.Sources.ZONE_1,
155
+ AnalogOutput.Sources.OPTICAL_IN,
156
+ ]
157
+
158
+ # A.3x
159
+ ANALOG_OUTPUT_SOURCES_FOR_3_ZONE_DEVICE = ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE + [
160
+ AnalogOutput.Sources.ZONE_2,
161
+ AnalogOutput.Sources.ZONE_3,
162
+ ]
163
+
164
+ # A.3
165
+ ANALOG_OUTPUT_SOURCES_FOR_A3 = ANALOG_OUTPUT_SOURCES_FOR_3_ZONE_DEVICE + [
166
+ AnalogOutput.Sources.BUS_IN_1
167
+ ]
168
+
169
+ # A.6x
170
+ ANALOG_OUTPUT_SOURCES_FOR_6_ZONE_DEVICE = ANALOG_OUTPUT_SOURCES_FOR_3_ZONE_DEVICE + [
171
+ AnalogOutput.Sources.ZONE_4,
172
+ AnalogOutput.Sources.ZONE_5,
173
+ AnalogOutput.Sources.ZONE_6,
174
+ ]
175
+
176
+ # A.6
177
+ ANALOG_OUTPUT_SOURCES_FOR_A6 = list(AnalogOutput.Sources)
178
+
179
+
180
+ class Features(VsslIntEnum):
181
+ GROUPING = 1000
182
+ BLUETOOTH = 1001
183
+ PARTY_MODE = 1002
184
+ SUBWOOFER_CROSSOVER = 1003
185
+
186
+
187
+ class Model:
188
+ def __init__(self, model: dict):
189
+ self.name = model.get("name")
190
+ self.zones = model.get("zones", [])
191
+ self.input_sources = model.get("input_sources", [])
192
+ self.analog_output_sources = model.get("analog_output_sources", [])
193
+ self.features = model.get("features", [])
194
+
195
+ @property
196
+ def zone_count(self):
197
+ return len(self.zones)
198
+
199
+ @property
200
+ def is_multizone(self):
201
+ return self.zone_count > 1
202
+
203
+ def supports_feature(self, feature: Features):
204
+ return feature in self.features
205
+
206
+
207
+ class Models(VsslEnum):
208
+ A1 = Model(
209
+ {
210
+ "name": "A.1",
211
+ "zones": SINGLE_ZONE,
212
+ "input_sources": INPUT_SOURCES_FOR_1_ZONE_DEVICE,
213
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE,
214
+ "features": [Features.BLUETOOTH, Features.SUBWOOFER_CROSSOVER],
215
+ }
216
+ )
217
+ A3 = Model(
218
+ {
219
+ "name": "A.3",
220
+ "zones": THREE_ZONES,
221
+ "input_sources": INPUT_SOURCES_FOR_A3,
222
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_A3,
223
+ "features": [Features.GROUPING, Features.PARTY_MODE],
224
+ }
225
+ )
226
+ A6 = Model(
227
+ {
228
+ "name": "A.6",
229
+ "zones": SIX_ZONES,
230
+ "input_sources": INPUT_SOURCES_FOR_A6,
231
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_A6,
232
+ "features": [Features.GROUPING, Features.PARTY_MODE],
233
+ }
234
+ )
235
+ A1X = Model(
236
+ {
237
+ "name": "A.1x",
238
+ "zones": SINGLE_ZONE,
239
+ "input_sources": INPUT_SOURCES_FOR_1_ZONE_DEVICE,
240
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE,
241
+ "features": [Features.BLUETOOTH, Features.SUBWOOFER_CROSSOVER],
242
+ }
243
+ )
244
+ A3X = Model(
245
+ {
246
+ "name": "A.3x",
247
+ "zones": THREE_ZONES,
248
+ "input_sources": INPUT_SOURCES_FOR_3_ZONE_DEVICE,
249
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_3_ZONE_DEVICE,
250
+ "features": [Features.GROUPING],
251
+ }
252
+ )
253
+ A6X = Model(
254
+ {
255
+ "name": "A.6x",
256
+ "zones": SIX_ZONES,
257
+ "input_sources": INPUT_SOURCES_FOR_6_ZONE_DEVICE,
258
+ "analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_6_ZONE_DEVICE,
259
+ "features": [Features.GROUPING],
260
+ }
261
+ )
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- """ Scan for apple devices. """
3
+ """ UNTESTED AND IN DEV """
4
4
 
5
5
  import json
6
6
  import asyncio
@@ -9,8 +9,8 @@ class InputRouter(ZoneDataClass):
9
9
  #
10
10
  # Input Priority
11
11
  #
12
- # 0: Stream -> Analog Input
13
- # 1: Analog Input -> Stream (Local first)
12
+ # 0: Stream -> Party Zone -> Bus 1 In -> Bus 2 In -> Analog Input (Stream First)
13
+ # 1: Bus 1 In -> Bus 2 In -> Analog Input -> Stream -> Party Zone (Local First)
14
14
  #
15
15
  # DO NOT CHANGE - VSSL Defined
16
16
  #
@@ -25,6 +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
30
  ANALOG_IN_1 = 3
29
31
  ANALOG_IN_2 = 4
30
32
  ANALOG_IN_3 = 5
@@ -49,8 +51,8 @@ class InputRouter(ZoneDataClass):
49
51
  def __init__(self, zone: "zone.Zone"):
50
52
  self.zone = zone
51
53
 
52
- self._priority = self.Priorities.STREAM
53
- self._source = self.Sources.STREAM
54
+ self._priority = self.DEFAULTS["priority"]
55
+ self._source = self.DEFAULTS["source"]
54
56
 
55
57
  #
56
58
  # Input Priority
@@ -103,8 +105,14 @@ class InputRouter(ZoneDataClass):
103
105
 
104
106
  class AnalogOutput(ZoneDataClass):
105
107
  """
108
+
109
+ Analog output is the source which will play out the corrosponding analog output
110
+
111
+
106
112
  Should this be on the VSSL or Zone? For now its on the zone, because the zone will
107
113
  receive feedback for the corrosponding analog output id
114
+
115
+ Zones will be determined by source Input Priority
108
116
  """
109
117
 
110
118
  #
@@ -114,6 +122,8 @@ class AnalogOutput(ZoneDataClass):
114
122
  #
115
123
  class Sources(VsslIntEnum):
116
124
  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
117
127
  ZONE_1 = 3
118
128
  ZONE_2 = 4
119
129
  ZONE_3 = 5
@@ -138,7 +148,7 @@ class AnalogOutput(ZoneDataClass):
138
148
  def __init__(self, zone: "zone.Zone"):
139
149
  self.zone = zone
140
150
 
141
- self._is_fixed_volume = False
151
+ self._is_fixed_volume = self.DEFAULTS["is_fixed_volume"]
142
152
  self._source = self.Sources(zone.id + 2)
143
153
 
144
154
  #
@@ -158,6 +168,8 @@ class AnalogOutput(ZoneDataClass):
158
168
  #
159
169
  # Analog Output Source
160
170
  #
171
+ # As in what audio is going to be sent to the analog output.
172
+ #
161
173
  @property
162
174
  def source(self):
163
175
  return self._source
@@ -199,7 +211,7 @@ class AnalogInput(ZoneDataClass):
199
211
  self.zone = zone
200
212
 
201
213
  self._name = f"Analog In {self.zone.id}"
202
- self._fixed_gain = 0
214
+ self._fixed_gain = self.DEFAULTS["fixed_gain"]
203
215
 
204
216
  #
205
217
  # Analog Input Name
@@ -2,6 +2,7 @@ import logging
2
2
  from typing import Dict, Union
3
3
  from .utils import clamp_volume
4
4
  from .io import AnalogInput
5
+ from .device import Features as DeviceFeatures
5
6
  from .data_structure import (
6
7
  VsslIntEnum,
7
8
  VsslDataClass,
@@ -16,6 +17,7 @@ class VsslSettings(VsslDataClass):
16
17
  class Keys:
17
18
  NAME = "name"
18
19
  OPTICAL_INPUT_NAME = "optical_input_name"
20
+ BLUETOOTH = "bluetooth"
19
21
 
20
22
  #
21
23
  # VSSL Events
@@ -24,17 +26,23 @@ class VsslSettings(VsslDataClass):
24
26
  PREFIX = VSSL_SETTINGS_EVENT_PREFIX
25
27
  NAME_CHANGE = PREFIX + "name_changed"
26
28
  OPTICAL_INPUT_NAME_CHANGE = PREFIX + "optical_input_name_changed"
29
+ BLUETOOTH_CHANGE = PREFIX + "bluetooth_changed"
27
30
 
28
31
  #
29
32
  # Defaults
30
33
  #
31
- DEFAULTS = {Keys.NAME: None, Keys.OPTICAL_INPUT_NAME: "Optical In"}
34
+ DEFAULTS = {
35
+ Keys.NAME: None,
36
+ Keys.OPTICAL_INPUT_NAME: "Optical In",
37
+ Keys.BLUETOOTH: 0,
38
+ }
32
39
 
33
40
  def __init__(self, vssl: "vsslctrl.Vssl"):
34
41
  self._vssl = vssl
35
42
 
36
43
  self._name = None # device name
37
44
  self._optical_input_name = self.DEFAULTS[self.Keys.OPTICAL_INPUT_NAME]
45
+ self._bluetooth = self.DEFAULTS[self.Keys.BLUETOOTH]
38
46
  self.power = VsslPowerSettings(vssl)
39
47
 
40
48
  #
@@ -63,6 +71,30 @@ class VsslSettings(VsslDataClass):
63
71
  if zone:
64
72
  zone.api_alpha.request_action_15_12(name)
65
73
 
74
+ #
75
+ # Bluetooth
76
+ #
77
+ @property
78
+ def bluetooth(self):
79
+ return bool(self._bluetooth)
80
+
81
+ @bluetooth.setter
82
+ def bluetooth(self, enabled: bool):
83
+ if not self._vssl.model.supports_feature(DeviceFeatures.BLUETOOTH):
84
+ self._vssl._log_error(
85
+ f"VSSL {self._vssl.model.name} does not support Bluetooth"
86
+ )
87
+ return
88
+
89
+ zone = self._vssl.get_connected_zone()
90
+ if zone:
91
+ # TODO
92
+ # zone.api_alpha.request_action_bluetooth(not not enabled)
93
+ self._vssl._log_info(f"TODO - bluetooth set function")
94
+
95
+ def bluetooth_toggle(self):
96
+ self.bluetooth = False if self.bluetooth else True
97
+
66
98
 
67
99
  class VsslPowerSettings(VsslDataClass):
68
100
  #
@@ -86,13 +118,13 @@ class VsslPowerSettings(VsslDataClass):
86
118
  #
87
119
  # Defaults
88
120
  #
89
- DEFAULTS = {"state": States.ON, "adaptive": False}
121
+ DEFAULTS = {"state": States.ON, "adaptive": True}
90
122
 
91
123
  def __init__(self, vssl: "vsslctrl.Vssl"):
92
124
  self._vssl = vssl
93
125
 
94
- self._state = self.States.ON
95
- self._adaptive = True # 1 = auto, 0 = always on
126
+ self._state = self.DEFAULTS["state"]
127
+ self._adaptive = self.DEFAULTS["adaptive"] # 1 = auto, 0 = always on
96
128
 
97
129
  #
98
130
  # Power State
@@ -165,6 +197,7 @@ class ZoneSettings(ZoneDataClass):
165
197
  self._mono = self.StereoMono.Stereo
166
198
 
167
199
  self.eq = EQSettings(zone)
200
+ self.subwoofer = SubwooferSettings(zone)
168
201
  self.volume = VolumeSettings(zone)
169
202
  self.analog_input = AnalogInput(zone)
170
203
 
@@ -653,3 +686,60 @@ class EQSettings(ZoneDataClass):
653
686
  @khz15_db.setter
654
687
  def khz15_db(self, val: int):
655
688
  self._set_frequency_on_device_db(self.Freqs.KHZ15, val)
689
+
690
+
691
+ # Not in io.py to prevent circ import in device.py
692
+ class SubwooferSettings(ZoneDataClass):
693
+ MIN_VALUE = 0
694
+ MAX_VALUE = 1000
695
+
696
+ #
697
+ # Subwoofer events
698
+ #
699
+ class Events:
700
+ PREFIX = "zone.subwoofer."
701
+ CROSSOVER_CHANGE = PREFIX + "crossover_change"
702
+
703
+ #
704
+ # Defaults
705
+ #
706
+ DEFAULTS = {"crossover": 0}
707
+
708
+ def __init__(self, zone: "zone.Zone"):
709
+ self.zone = zone
710
+
711
+ self._crossover = self.DEFAULTS["crossover"]
712
+
713
+ #
714
+ # Clamp between min and max
715
+ #
716
+ def _clamp_crossover(self, value: int = 100):
717
+ return int(max(self.MIN_VALUE, min(value, self.MAX_VALUE)))
718
+
719
+ #
720
+ # Crossover freq
721
+ #
722
+ @property
723
+ def crossover(self):
724
+ return self._crossover
725
+
726
+ @crossover.setter
727
+ def crossover(self, freq: int):
728
+ if not self.zone.vssl.model.supports_feature(
729
+ DeviceFeatures.SUBWOOFER_CROSSOVER
730
+ ):
731
+ self.zone._log_error(
732
+ f"VSSL {self.zone.vssl.model.name} does not have a subwoofer output"
733
+ )
734
+ return
735
+ #
736
+ # TODO
737
+ #
738
+ self.zone._log_info(f"TODO - crossover set function")
739
+ # self.zone.api_alpha.request_action_crossover(freq)
740
+
741
+ def _set_crossover(self, freq: int):
742
+ freq = self._clamp_crossover(freq)
743
+ if self.crossover != freq:
744
+ self._crossover = freq
745
+ return True
@@ -74,7 +74,7 @@ class Zone:
74
74
  self._request_eq_status,
75
75
  self._request_track,
76
76
  self._request_name,
77
- self._request_bt_status,
77
+ self._request_status_extended,
78
78
  ],
79
79
  )
80
80
 
@@ -128,6 +128,8 @@ class Zone:
128
128
  self.initialisation.set()
129
129
  self.vssl.event_bus.publish(self.Events.INITIALISED, self.id, self)
130
130
 
131
+ self._log_info(f"Zone {self.id} initialised")
132
+
131
133
  return self
132
134
 
133
135
  @property
@@ -406,7 +408,7 @@ class Zone:
406
408
  self.api_alpha.request_action_00_0A()
407
409
  return self
408
410
 
409
- def _request_bt_status(self):
411
+ def _request_status_extended(self):
410
412
  self.api_alpha.request_action_00_0B()
411
413
  return self
412
414
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vsslctrl
3
- Version: 0.1.6.dev1
3
+ Version: 0.1.8.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
@@ -247,6 +247,13 @@ zone1.transport.state = ZoneTransport.States.PAUSE
247
247
  | `source` | Change input source | `int` | `InputRouter.Sources`
248
248
  | `priority` | Change input priority. Stream or analog in higher priority | `int` | `InputRouter.Priorities`
249
249
 
250
+ ### Priority Order:
251
+
252
+ | `InputRouter.Priorities` | Priority Order |
253
+ | ---------------------- | ----------- |
254
+ | **`STREAM`** | <ol><li>Stream</li><li>Party Zone</li><li>Bus 1 In</li><li>Bus 2 In</li><li>Analog Input</li></ol>
255
+ | **`LOCAL`** | <ol><li>Bus 1 In</li><li>Bus 2 In</li><li>Analog Input</li><li>Stream</li><li>Party Zone</li></ol>
256
+
250
257
  ```python
251
258
  """Example"""
252
259
  # Change zone 1 to listen to analog input 4
@@ -1,115 +0,0 @@
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
- )
File without changes
File without changes