vsslctrl 0.1.3.dev1__py3-none-any.whl → 0.1.5.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 +2 -0
- vsslctrl/api_alpha.py +2 -2
- vsslctrl/api_base.py +13 -0
- vsslctrl/core.py +37 -51
- vsslctrl/data_structure.py +193 -56
- vsslctrl/device.py +115 -0
- vsslctrl/group.py +13 -7
- vsslctrl/io.py +12 -5
- vsslctrl/settings.py +1 -1
- vsslctrl/zone.py +3 -14
- {vsslctrl-0.1.3.dev1.dist-info → vsslctrl-0.1.5.dev1.dist-info}/METADATA +78 -21
- vsslctrl-0.1.5.dev1.dist-info/RECORD +23 -0
- {vsslctrl-0.1.3.dev1.dist-info → vsslctrl-0.1.5.dev1.dist-info}/WHEEL +1 -1
- vsslctrl-0.1.3.dev1.dist-info/RECORD +0 -22
- {vsslctrl-0.1.3.dev1.dist-info → vsslctrl-0.1.5.dev1.dist-info}/LICENSE +0 -0
- {vsslctrl-0.1.3.dev1.dist-info → vsslctrl-0.1.5.dev1.dist-info}/top_level.txt +0 -0
vsslctrl/__init__.py
CHANGED
vsslctrl/api_alpha.py
CHANGED
|
@@ -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
|
-
#
|
|
562
|
-
self.vssl.
|
|
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)
|
vsslctrl/api_base.py
CHANGED
|
@@ -158,6 +158,19 @@ class APIBase(ABC):
|
|
|
158
158
|
|
|
159
159
|
except asyncio.CancelledError:
|
|
160
160
|
self._log_debug(f"writer close timeout")
|
|
161
|
+
except asyncio.TimeoutError as e:
|
|
162
|
+
self._log_error(f"Timeout while closing connection: {e}")
|
|
163
|
+
# Handle the timeout: log, retry, or take other actions
|
|
164
|
+
except ConnectionResetError as e:
|
|
165
|
+
self._log_error(f"Connection reset by peer: {e}")
|
|
166
|
+
# Handle the error: log, retry, or take other actions
|
|
167
|
+
except Exception as e:
|
|
168
|
+
self._log_error(f"Unexpected error occurred while disconnecting: {e}")
|
|
169
|
+
# Handle unexpected errors
|
|
170
|
+
finally:
|
|
171
|
+
self._writer = None
|
|
172
|
+
|
|
173
|
+
self._reader = None
|
|
161
174
|
|
|
162
175
|
self._log_info(f"{self.host}:{self.port}: disconnected")
|
|
163
176
|
|
vsslctrl/core.py
CHANGED
|
@@ -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 .
|
|
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:
|
|
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
|
-
|
|
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
|
|
66
|
-
#
|
|
67
|
-
if not self.
|
|
68
|
-
|
|
69
|
-
self.Events.
|
|
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
|
|
75
|
+
# Only continue after we have a model
|
|
76
76
|
try:
|
|
77
|
-
if not self.
|
|
78
|
-
await asyncio.wait_for(
|
|
77
|
+
if not self.model:
|
|
78
|
+
await asyncio.wait_for(future_model, timeout=init_timeout)
|
|
79
79
|
|
|
80
|
-
if len(self.zones) > self.
|
|
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
|
-
|
|
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
|
|
177
|
-
if
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
self.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
190
|
+
# Work out the a model given some device info
|
|
202
191
|
#
|
|
203
|
-
def
|
|
204
|
-
if
|
|
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.
|
|
200
|
+
self.model = f"A{zone_count}X"
|
|
211
201
|
else:
|
|
212
|
-
self.
|
|
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:
|
|
238
|
-
if
|
|
239
|
-
error = f"
|
|
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:
|
|
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:
|
vsslctrl/data_structure.py
CHANGED
|
@@ -29,33 +29,50 @@ class VsslIntEnum(VsslEnum, IntEnum):
|
|
|
29
29
|
"""IntEnum"""
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
39
44
|
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
""" JSON Structure
|
|
53
|
-
|
|
54
|
-
DO NOT CHANGE - VSSL Defined
|
|
55
|
-
|
|
56
|
-
{'B1Src': '3', 'B2Src': '4', 'B3Src': '5', 'B1Nm': '', 'B2Nm': 'Optical In', 'dev': 'Device Name', 'ver': 'p15305.016.3701'}
|
|
57
45
|
|
|
58
46
|
"""
|
|
47
|
+
JSON Structure
|
|
48
|
+
|
|
49
|
+
DO NOT CHANGE - VSSL Defined
|
|
50
|
+
|
|
51
|
+
A3.x
|
|
52
|
+
{
|
|
53
|
+
"B1Src": "3",
|
|
54
|
+
"B2Src": "4",
|
|
55
|
+
"B3Src": "5",
|
|
56
|
+
"B1Nm": "",
|
|
57
|
+
"B2Nm": "Optical In",
|
|
58
|
+
"dev": "Device Name",
|
|
59
|
+
"ver": "p15305.016.3701"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
A6.x
|
|
63
|
+
{
|
|
64
|
+
"B1Src": "3",
|
|
65
|
+
"B2Src": "4",
|
|
66
|
+
"B3Src": "5",
|
|
67
|
+
"B4Src": "6",
|
|
68
|
+
"B5Src": "7",
|
|
69
|
+
"B6Src": "8",
|
|
70
|
+
"B1Nm": "",
|
|
71
|
+
"B2Nm": "",
|
|
72
|
+
"dev": "VSSL A.6x",
|
|
73
|
+
"ver": "p15305.017.3701"
|
|
74
|
+
}
|
|
75
|
+
"""
|
|
59
76
|
|
|
60
77
|
|
|
61
78
|
class DeviceStatusExtKeys:
|
|
@@ -75,13 +92,50 @@ class DeviceStatusExtKeys:
|
|
|
75
92
|
return f"B{zone_id}Src"
|
|
76
93
|
|
|
77
94
|
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
"""
|
|
96
|
+
JSON Structure
|
|
97
|
+
|
|
98
|
+
DO NOT CHANGE - VSSL Defined
|
|
99
|
+
|
|
100
|
+
A3.x
|
|
101
|
+
{
|
|
102
|
+
"id": "1",
|
|
103
|
+
"ac": "0",
|
|
104
|
+
"mc": "XXXXXXXXXXXX",
|
|
105
|
+
"vol": "20",
|
|
106
|
+
"mt": "0",
|
|
107
|
+
"pa": "0",
|
|
108
|
+
"rm": "0",
|
|
109
|
+
"ts": "14",
|
|
110
|
+
"alex": "14",
|
|
111
|
+
"nmd": "0",
|
|
112
|
+
"ird": "14",
|
|
113
|
+
"lb": "24",
|
|
114
|
+
"tp": "13",
|
|
115
|
+
"wr": "0",
|
|
116
|
+
"as": "0",
|
|
117
|
+
"rg": "0"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
A6.x
|
|
121
|
+
{
|
|
122
|
+
"id": "1",
|
|
123
|
+
"ac": "0",
|
|
124
|
+
"mc": "XXXXXXXXXXXX",
|
|
125
|
+
"vol": "50",
|
|
126
|
+
"mt": "0",
|
|
127
|
+
"pa": "0",
|
|
128
|
+
"rm": "0",
|
|
129
|
+
"ts": "0",
|
|
130
|
+
"alex": "126",
|
|
131
|
+
"nmd": "0",
|
|
132
|
+
"ird": "255",
|
|
133
|
+
"lb": "17",
|
|
134
|
+
"tp": "16",
|
|
135
|
+
"wr": "0",
|
|
136
|
+
"as": "0",
|
|
137
|
+
"rg": "0"
|
|
138
|
+
}
|
|
85
139
|
"""
|
|
86
140
|
|
|
87
141
|
|
|
@@ -97,13 +151,42 @@ class ZoneStatusExtKeys:
|
|
|
97
151
|
DISABLED = "wr"
|
|
98
152
|
|
|
99
153
|
|
|
100
|
-
"""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
154
|
+
"""
|
|
155
|
+
JSON Structure
|
|
156
|
+
|
|
157
|
+
DO NOT CHANGE - VSSL Defined
|
|
158
|
+
|
|
159
|
+
A3.x
|
|
160
|
+
{
|
|
161
|
+
"mono": "0",
|
|
162
|
+
"AiNm": "Analog In 1",
|
|
163
|
+
"eq1": "100",
|
|
164
|
+
"eq2": "100",
|
|
165
|
+
"eq3": "100",
|
|
166
|
+
"eq4": "100",
|
|
167
|
+
"eq5": "100",
|
|
168
|
+
"eq6": "100",
|
|
169
|
+
"eq7": "100",
|
|
170
|
+
"voll": "75",
|
|
171
|
+
"volr": "75",
|
|
172
|
+
"vold": "0"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
A6.x
|
|
176
|
+
{
|
|
177
|
+
"mono": "0",
|
|
178
|
+
"AiNm": "",
|
|
179
|
+
"eq1": "100",
|
|
180
|
+
"eq2": "100",
|
|
181
|
+
"eq3": "100",
|
|
182
|
+
"eq4": "100",
|
|
183
|
+
"eq5": "100",
|
|
184
|
+
"eq6": "100",
|
|
185
|
+
"eq7": "100",
|
|
186
|
+
"voll": "75",
|
|
187
|
+
"volr": "75",
|
|
188
|
+
"vold": "0"
|
|
189
|
+
}
|
|
107
190
|
"""
|
|
108
191
|
|
|
109
192
|
|
|
@@ -122,13 +205,47 @@ class ZoneEQStatusExtKeys:
|
|
|
122
205
|
VOL_DEFAULT_ON = "vold"
|
|
123
206
|
|
|
124
207
|
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
208
|
+
"""
|
|
209
|
+
JSON Structure
|
|
210
|
+
|
|
211
|
+
DO NOT CHANGE - VSSL Defined
|
|
212
|
+
|
|
213
|
+
A3.x
|
|
214
|
+
{
|
|
215
|
+
"ECO": "0",
|
|
216
|
+
"eqsw": "1",
|
|
217
|
+
"inSrc": "0",
|
|
218
|
+
"SP": "0",
|
|
219
|
+
"BF1": "0",
|
|
220
|
+
"BF2": "0",
|
|
221
|
+
"BF3": "0",
|
|
222
|
+
"GRM": "0",
|
|
223
|
+
"GRS": "255",
|
|
224
|
+
"Pwr": "0",
|
|
225
|
+
"Bvr": "1",
|
|
226
|
+
"fxv": "24",
|
|
227
|
+
"AtPwr": "1"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
A6.x
|
|
231
|
+
{
|
|
232
|
+
"ECO": "0",
|
|
233
|
+
"eqsw": "1",
|
|
234
|
+
"inSrc": "0",
|
|
235
|
+
"SP": "0",
|
|
236
|
+
"BF1": "0",
|
|
237
|
+
"BF2": "0",
|
|
238
|
+
"BF3": "0",
|
|
239
|
+
"BF4": "0",
|
|
240
|
+
"BF5": "0",
|
|
241
|
+
"BF6": "0",
|
|
242
|
+
"GRM": "0",
|
|
243
|
+
"GRS": "255",
|
|
244
|
+
"Pwr": "0",
|
|
245
|
+
"Bvr": "2",
|
|
246
|
+
"fxv": "25",
|
|
247
|
+
"AtPwr": "1"
|
|
248
|
+
}
|
|
132
249
|
"""
|
|
133
250
|
|
|
134
251
|
|
|
@@ -148,17 +265,37 @@ class ZoneRouterStatusExtKeys:
|
|
|
148
265
|
return f"BF{zone_id}"
|
|
149
266
|
|
|
150
267
|
|
|
151
|
-
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
268
|
+
"""
|
|
269
|
+
JSON Structure
|
|
270
|
+
|
|
271
|
+
DO NOT CHANGE - VSSL Defined
|
|
272
|
+
|
|
273
|
+
{
|
|
274
|
+
"Album": "International Skankers",
|
|
275
|
+
"Artist": "Ashkabad",
|
|
276
|
+
"BitDepth": 16,
|
|
277
|
+
"BitRate": "320000",
|
|
278
|
+
"CoverArtUrl": "https://i.scdn.co/image/ab67616d0000b2730cbb03a339c6ffd18d10eab2",
|
|
279
|
+
"Current Source": 4,
|
|
280
|
+
"Current_time": -1,
|
|
281
|
+
"DSDType": "",
|
|
282
|
+
"Fav": False,
|
|
283
|
+
"FileSize": 0,
|
|
284
|
+
"Genre": "",
|
|
285
|
+
"Index": 0,
|
|
286
|
+
"Mime": "Ogg",
|
|
287
|
+
"Next": False,
|
|
288
|
+
"PlayState": 0,
|
|
289
|
+
"PlayUrl": "spotify:track:0IHTiLO5qBYhf7Hmn0UDBN",
|
|
290
|
+
"Prev": False,
|
|
291
|
+
"Repeat": 0,
|
|
292
|
+
"SampleRate": "44100",
|
|
293
|
+
"Seek": False,
|
|
294
|
+
"Shuffle": 0,
|
|
295
|
+
"SinglePlay": False,
|
|
296
|
+
"TotalTime": 203087,
|
|
297
|
+
"TrackName": "Beijing"
|
|
298
|
+
}
|
|
162
299
|
"""
|
|
163
300
|
|
|
164
301
|
|
vsslctrl/device.py
ADDED
|
@@ -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
|
+
)
|
vsslctrl/group.py
CHANGED
|
@@ -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:
|
|
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
|
|
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:
|
|
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
|
vsslctrl/io.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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:
|
|
@@ -135,7 +139,7 @@ class AnalogOutput(ZoneDataClass):
|
|
|
135
139
|
self.zone = zone
|
|
136
140
|
|
|
137
141
|
self._is_fixed_volume = False
|
|
138
|
-
self._source = self.Sources(zone.id +
|
|
142
|
+
self._source = self.Sources(zone.id + 2)
|
|
139
143
|
|
|
140
144
|
#
|
|
141
145
|
# Analog Output Fix Volume. Output wont respond to volume control
|
|
@@ -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(
|
|
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:
|
vsslctrl/settings.py
CHANGED
vsslctrl/zone.py
CHANGED
|
@@ -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
|
|
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:
|
|
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 =
|
|
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.
|
|
3
|
+
Version: 0.1.5.dev1
|
|
4
4
|
Summary: Package for controlling VSSL amplifiers
|
|
5
5
|
Author-email: vsslctrl <vsslcontrolled@proton.me>
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -28,37 +28,45 @@ Requires-Dist: zeroconf ==0.132.2 ; extra == 'optional'
|
|
|
28
28
|
|
|
29
29
|
I am looking for testers with any VSSL amplifier models, please get in touch if you interested in helping.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Important
|
|
32
|
+
-----------
|
|
33
|
+
Only tested on a VSSL **A.3x** software version **p15305.016.3701**.
|
|
34
|
+
|
|
35
|
+
**Notice:** VSSLs new iOS app titled `VSSL`can cause connection refused issues if running at the same time as `vsslctrl`. Best to not use the iOS app togeather. The `VSSL Legacy` app works fine
|
|
36
|
+
|
|
37
|
+
**Warning:** no *[VSSL Agent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start)* should be running on the same network. If you dont know what this is, then you can probably ignore this notice.
|
|
32
38
|
|
|
33
39
|
## TODOs
|
|
34
40
|
|
|
35
41
|
* Test on other models (hardware needed)
|
|
36
42
|
* Home Assistant integration. In progress, [here](https://github.com/vsslctrl/integration.home-assistant)
|
|
37
|
-
* Function scoping to supported feature / models
|
|
43
|
+
* ~~Function scoping to supported feature / models~~ (needs tests)
|
|
38
44
|
* Better test coverage
|
|
39
45
|
|
|
40
|
-
Important
|
|
41
|
-
-----------
|
|
42
|
-
**Warning:** no *[VSSL Agent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start)* should be running on the same network. If you dont know what this is, then you can probably ignore this notice.
|
|
43
|
-
|
|
44
|
-
|
|
45
46
|
Basic Usage
|
|
46
47
|
-----------
|
|
47
48
|
`vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
|
|
48
49
|
|
|
49
50
|
```python
|
|
50
51
|
import asyncio
|
|
51
|
-
from vsslctrl import Vssl, Zone
|
|
52
|
+
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
52
53
|
|
|
53
54
|
async def main():
|
|
54
55
|
|
|
55
56
|
# Represents a physical VSSL amplifier
|
|
56
57
|
vssl = Vssl()
|
|
57
58
|
|
|
59
|
+
"""Optional to init Vssl with a device model
|
|
60
|
+
|
|
61
|
+
vssl = Vssl(DeviceModels.A3X)
|
|
62
|
+
|
|
63
|
+
If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
|
|
64
|
+
"""
|
|
65
|
+
|
|
58
66
|
# Add each you wish to control
|
|
59
|
-
zone1 = vssl.add_zone(
|
|
60
|
-
zone2 = vssl.add_zone(
|
|
61
|
-
zone3 = vssl.add_zone(
|
|
67
|
+
zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
|
|
68
|
+
zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
|
|
69
|
+
zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
|
|
62
70
|
#... up to 6 zones
|
|
63
71
|
|
|
64
72
|
# Connect and initiate zones.
|
|
@@ -110,7 +118,7 @@ print(zone_name)
|
|
|
110
118
|
| ---------------------- | ----------- | ----------- |
|
|
111
119
|
| `sw_version` | Software version | `str` readonly
|
|
112
120
|
| `serial` | Serial number | `str` readonly
|
|
113
|
-
| `
|
|
121
|
+
| `model` | Device Model | `int` readonly
|
|
114
122
|
| `reboot()` | Reboot all zones | `func` |
|
|
115
123
|
|
|
116
124
|
```python
|
|
@@ -152,7 +160,7 @@ vssl.settings.power.adaptive = True
|
|
|
152
160
|
|
|
153
161
|
| Property | Description | Type | Values |
|
|
154
162
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
155
|
-
| `id` | Zone number / ID | `int` readonly | `
|
|
163
|
+
| `id` | Zone number / ID | `int` readonly | `ZoneIDs`
|
|
156
164
|
| `host` | IP address | `str` readonly
|
|
157
165
|
| `volume` | Volume | `int` | `0...100`
|
|
158
166
|
| `volume_raise([step=1])` | Raise volume by `step` | `func` | step: `int` `1...100`
|
|
@@ -219,7 +227,7 @@ zone1.transport.state = ZoneTransport.States.PAUSE
|
|
|
219
227
|
## `Zone.track`
|
|
220
228
|
|
|
221
229
|
* Not all sources have complete metadata - missing value will be set to defaults.
|
|
222
|
-
* Airplay track
|
|
230
|
+
* Airplay track `progress` is not avaiable.
|
|
223
231
|
|
|
224
232
|
| Property | Description | Type | Values |
|
|
225
233
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
@@ -256,20 +264,20 @@ Working on A.3x but offically unsupported in x series amplifiers.
|
|
|
256
264
|
|
|
257
265
|
| Property | Description | Type | Values |
|
|
258
266
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
259
|
-
| `source` | Zone ID of group master / source | `int` readonly | `
|
|
267
|
+
| `source` | Zone ID of group master / source | `int` readonly | `ZoneIDs`
|
|
260
268
|
| `is_master` | This zone is the group master | `bool` readonly
|
|
261
|
-
| `add_member()` | Add zone to group / create group | `func` | `
|
|
262
|
-
| `remove_member()` | Remove zone from group | `func` | `
|
|
269
|
+
| `add_member()` | Add zone to group / create group | `func` | `ZoneIDs`
|
|
270
|
+
| `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
|
|
263
271
|
| `dissolve()` | Dissolve group / remove all members | `func` |
|
|
264
272
|
| `leave()` | Leave the group if a member | `func` |
|
|
265
273
|
|
|
266
274
|
```python
|
|
267
275
|
"""Examples"""
|
|
268
276
|
# Add group 2 to a group with zone 1 as master
|
|
269
|
-
zone1.group.add_member(
|
|
270
|
-
# Remove zone 2 from group
|
|
277
|
+
zone1.group.add_member(ZoneIDs.ZONE_2)
|
|
278
|
+
# Remove zone 2 from group
|
|
271
279
|
zone2.group.leave() # or
|
|
272
|
-
zone1.group.remove_member(
|
|
280
|
+
zone1.group.remove_member(ZoneIDs.ZONE_2)
|
|
273
281
|
# If zone 1 is a master, remove all members
|
|
274
282
|
zone1.group.dissolve()
|
|
275
283
|
```
|
|
@@ -371,6 +379,53 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
|
|
|
371
379
|
zone1.settings.eq.khz1_db = -2
|
|
372
380
|
```
|
|
373
381
|
|
|
382
|
+
## Another (Lite) Way
|
|
383
|
+
|
|
384
|
+
If you perfer to not run the complete intergration, you can send basic HEX commands to the VSSL device using [Netcat](https://nc110.sourceforge.io/) (or any network tool) on port `50002`.
|
|
385
|
+
|
|
386
|
+
| HEX | Description |
|
|
387
|
+
| ---------------------- | ----------- |
|
|
388
|
+
| `\x10\x05\x03\x0{Zone Number}\xff\x03` | Volume Up
|
|
389
|
+
| `\x10\x05\x03\x0{Zone Number}\xfe\x03` | Volume Down
|
|
390
|
+
| `\x10\x11\x02\x0{Zone Number}\x01` | Mute
|
|
391
|
+
| All commands can be found by looking [here](https://github.com/vsslctrl/vsslctrl/blob/2c43c2f2393b94bc0e062d2ab90144343eca16ef/vsslctrl/api_alpha.py) |
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
The `volume up` HEX command for `Zone 2` would be `\x10\x05\x03\x02\xff\x03`
|
|
395
|
+
|
|
396
|
+
Now send the raw HEX using Netcat to the device using this syntax:
|
|
397
|
+
|
|
398
|
+
`echo -e "{HEX Command}" | nc {IP Address} 50002`
|
|
399
|
+
|
|
400
|
+
For example to send `volume up` to `Zone 2`:
|
|
401
|
+
|
|
402
|
+
`echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002`
|
|
403
|
+
|
|
404
|
+
Home Assistant `Configuration.yaml` example:
|
|
405
|
+
|
|
406
|
+
```ymal
|
|
407
|
+
...
|
|
408
|
+
|
|
409
|
+
shell_command:
|
|
410
|
+
#Zone 1
|
|
411
|
+
vssl_zone_1_volume_up: 'echo -e "\x10\x05\x03\x01\xff\x03" | nc 192.168.1.10 50002'
|
|
412
|
+
vssl_zone_1_volume_down: 'echo -e "\x10\x05\x03\x01\xfe\x03" | nc 192.168.1.10 50002'
|
|
413
|
+
vssl_zone_1_mute: 'echo -e "\x10\x11\x02\x01\x01" | nc 192.168.1.10 50002'
|
|
414
|
+
vssl_zone_1_unmute: 'echo -e "\x10\x11\x02\x01\x00" | nc 192.168.1.10 50002'
|
|
415
|
+
#Zone 2
|
|
416
|
+
vssl_zone_2_volume_up: 'echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002'
|
|
417
|
+
vssl_zone_2_volume_down: 'echo -e "\x10\x05\x03\x02\xfe\x03" | nc 192.168.1.11 50002'
|
|
418
|
+
vssl_zone_2_mute: 'echo -e "\x10\x11\x02\x02\x01" | nc 192.168.1.11 50002'
|
|
419
|
+
vssl_zone_2_unmute: 'echo -e "\x10\x11\x02\x02\x00" | nc 192.168.1.11 50002'
|
|
420
|
+
#Zone 3
|
|
421
|
+
vssl_zone_3_volume_up: 'echo -e "\x10\x05\x03\x03\xff\x03" | nc 192.168.1.12 50002'
|
|
422
|
+
vssl_zone_3_volume_down: 'echo -e "\x10\x05\x03\x03\xfe\x03" | nc 192.168.1.12 50002'
|
|
423
|
+
vssl_zone_3_mute: 'echo -e "\x10\x11\x02\x03\x01" | nc 192.168.1.12 50002'
|
|
424
|
+
vssl_zone_3_unmute: 'echo -e "\x10\x11\x02\x03\x00" | nc 192.168.1.12 50002'
|
|
425
|
+
|
|
426
|
+
...
|
|
427
|
+
```
|
|
428
|
+
|
|
374
429
|
## Credit
|
|
375
430
|
|
|
376
431
|
The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS app and their deprecated [vsslagent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start).
|
|
@@ -382,6 +437,7 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
|
|
|
382
437
|
* VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
|
|
383
438
|
* Not all sources set the volume to 0 when the zone is muted
|
|
384
439
|
* Grouping feedback is flaky on the X series amplifiers
|
|
440
|
+
* Airplay `Zone.track.progress` is not avaiable.
|
|
385
441
|
* Cant stop a URL playback, feedback is worng at least
|
|
386
442
|
* VSSL likes to cache old track metadata. For example when playing a URL after Spotify, often the device will respond with the previous (Spotify) tracks metadata
|
|
387
443
|
* `stop()` is intended to disconnect the client and pause the stream. Doesnt always function this way, depending on stream source
|
|
@@ -392,4 +448,5 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
|
|
|
392
448
|
* REST API / Web App
|
|
393
449
|
* Save and recall EQ
|
|
394
450
|
* A.1(x) coverage i.e Bluetooth
|
|
451
|
+
* IR Control
|
|
395
452
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
vsslctrl/__init__.py,sha256=W7cljoE7L3DukAVr_TEqkxFycYrUbJiqaHrOlbPkmKU,145
|
|
2
|
+
vsslctrl/api_alpha.py,sha256=wsrwc4owIlRZFJK40evdRDh-iY1ZOxuCjDfsy2xHlt8,33216
|
|
3
|
+
vsslctrl/api_base.py,sha256=a80CRtUkYK2OwXQOL8bP-YOvt4YE-ALd-VONza_gjK8,9623
|
|
4
|
+
vsslctrl/api_bravo.py,sha256=HW-toa38umSpKAij_zxyypsMoU8S_AXpAbBmAaRqgow,12196
|
|
5
|
+
vsslctrl/core.py,sha256=cMMfrZzSDtIO2J3__EsA6aAWsGVuxSWkfkC8byXpQSY,8823
|
|
6
|
+
vsslctrl/data_structure.py,sha256=RoOAB4VevWpiNJ-WdW3_s7onHTjLFenSzM2nLaV3vcA,7482
|
|
7
|
+
vsslctrl/decorators.py,sha256=lG8cYSSA-fNvdcp_90lv58tj50bF6iPoigLKcucWSSI,1707
|
|
8
|
+
vsslctrl/device.py,sha256=P4sYiibPPFp6iDuF93-OKIsWBL_9vtIxBPDc5uRwvbA,3418
|
|
9
|
+
vsslctrl/discovery.py,sha256=81uA3rbm-X_CR0Q0LsCQJyCs7638Bb-mgqNZWqV_dq4,5572
|
|
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=crzz5-vsdQfVFBL9a1Btle8mq5zx41SsVSt4N5cWPaE,6048
|
|
14
|
+
vsslctrl/settings.py,sha256=6zZUf2y-JvmEz6aREMZqxjTptMtVJFO6dGkXdCxR1NU,16587
|
|
15
|
+
vsslctrl/track.py,sha256=_lQNHYe8ZBm3v1lehFLjZruF5-Rz8PGWuQN-zISBgwc,10136
|
|
16
|
+
vsslctrl/transport.py,sha256=XJFy0kl9y7hm3YV4_9ElBrq3u6--_k73UKJqKdO6Rqg,6993
|
|
17
|
+
vsslctrl/utils.py,sha256=CqipPCffDjXCJ00NHAABOI886xRMyvnqkp4tr8i9uhg,1744
|
|
18
|
+
vsslctrl/zone.py,sha256=5UOgIslYLxEnZ9cjQc5nCgJF1ENlLMsoKcbjORW3hQk,12721
|
|
19
|
+
vsslctrl-0.1.5.dev1.dist-info/LICENSE,sha256=67Ekj8PIxjEvkPd7L3GYJ8dyvn1EC4sPM3q3FKtpN6w,1065
|
|
20
|
+
vsslctrl-0.1.5.dev1.dist-info/METADATA,sha256=mB3khdgA1cwsCVMu1nr1L-RyA-38q-SiMOVWcPGzX6o,16557
|
|
21
|
+
vsslctrl-0.1.5.dev1.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
|
|
22
|
+
vsslctrl-0.1.5.dev1.dist-info/top_level.txt,sha256=MRycO_RKageNX27UZ2bLJDMlfeWTQzUZlsd3hS-0u3Y,9
|
|
23
|
+
vsslctrl-0.1.5.dev1.dist-info/RECORD,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
vsslctrl/__init__.py,sha256=8neULYocxQjushISE5_1ziZIWSxFWuf2vseXGcrEmmk,66
|
|
2
|
-
vsslctrl/api_alpha.py,sha256=tgOG2vTth93HxAmtbDpXCOr1OcJ6dFsfyl3tSiqLMFU,33230
|
|
3
|
-
vsslctrl/api_base.py,sha256=BpDjKTopnWr9UXTPxOywri_2ndfV7m0MYiFk1nz4514,8998
|
|
4
|
-
vsslctrl/api_bravo.py,sha256=HW-toa38umSpKAij_zxyypsMoU8S_AXpAbBmAaRqgow,12196
|
|
5
|
-
vsslctrl/core.py,sha256=_WoB5uiRwzP5vi27dWtRgcNcd0WsWIsCT7LoIrtMqfE,9443
|
|
6
|
-
vsslctrl/data_structure.py,sha256=JYB8d96B49kj1XeSKisqCcBu723WcchlpC_-WDmOaaM,6557
|
|
7
|
-
vsslctrl/decorators.py,sha256=lG8cYSSA-fNvdcp_90lv58tj50bF6iPoigLKcucWSSI,1707
|
|
8
|
-
vsslctrl/discovery.py,sha256=81uA3rbm-X_CR0Q0LsCQJyCs7638Bb-mgqNZWqV_dq4,5572
|
|
9
|
-
vsslctrl/event_bus.py,sha256=9liAEJnZfthJwy5O-qc3pDIWRBv5AcV19PjMCwH_niU,5014
|
|
10
|
-
vsslctrl/exceptions.py,sha256=EEfNlu--9PMIidj3V3ZB8c7p3My8siqKw7YIOjPFaqE,419
|
|
11
|
-
vsslctrl/group.py,sha256=xHCvKR7nEbZRkO-oE0xd9oAkA_Vv_gSMIRvzQ2t3v28,5091
|
|
12
|
-
vsslctrl/io.py,sha256=UnuSNJeB8Mf1fy3GyrMVXaMhn7fIqnfVDtruwfxaC1M,5717
|
|
13
|
-
vsslctrl/settings.py,sha256=nkW4SpsVhYL3Ep6T7z5j2yQ0viWXW7NIZ3anc2WfYpA,16570
|
|
14
|
-
vsslctrl/track.py,sha256=_lQNHYe8ZBm3v1lehFLjZruF5-Rz8PGWuQN-zISBgwc,10136
|
|
15
|
-
vsslctrl/transport.py,sha256=XJFy0kl9y7hm3YV4_9ElBrq3u6--_k73UKJqKdO6Rqg,6993
|
|
16
|
-
vsslctrl/utils.py,sha256=CqipPCffDjXCJ00NHAABOI886xRMyvnqkp4tr8i9uhg,1744
|
|
17
|
-
vsslctrl/zone.py,sha256=-Uqt0picOp9SEwZT0s4tM11kkmnvj3MkkYjOo7lqXtc,12901
|
|
18
|
-
vsslctrl-0.1.3.dev1.dist-info/LICENSE,sha256=67Ekj8PIxjEvkPd7L3GYJ8dyvn1EC4sPM3q3FKtpN6w,1065
|
|
19
|
-
vsslctrl-0.1.3.dev1.dist-info/METADATA,sha256=c6aeDgtbpFKzQGeSy-sa2mDS_9RKr4GiFFIx8I3cToY,14078
|
|
20
|
-
vsslctrl-0.1.3.dev1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
21
|
-
vsslctrl-0.1.3.dev1.dist-info/top_level.txt,sha256=MRycO_RKageNX27UZ2bLJDMlfeWTQzUZlsd3hS-0u3Y,9
|
|
22
|
-
vsslctrl-0.1.3.dev1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|