vsslctrl 0.1.3.dev1__py3-none-any.whl → 0.1.4.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/core.py +37 -51
- vsslctrl/data_structure.py +12 -18
- vsslctrl/device.py +115 -0
- vsslctrl/group.py +13 -7
- vsslctrl/io.py +11 -4
- vsslctrl/settings.py +1 -1
- vsslctrl/zone.py +3 -14
- {vsslctrl-0.1.3.dev1.dist-info → vsslctrl-0.1.4.dev1.dist-info}/METADATA +74 -20
- vsslctrl-0.1.4.dev1.dist-info/RECORD +23 -0
- vsslctrl-0.1.3.dev1.dist-info/RECORD +0 -22
- {vsslctrl-0.1.3.dev1.dist-info → vsslctrl-0.1.4.dev1.dist-info}/LICENSE +0 -0
- {vsslctrl-0.1.3.dev1.dist-info → vsslctrl-0.1.4.dev1.dist-info}/WHEEL +0 -0
- {vsslctrl-0.1.3.dev1.dist-info → vsslctrl-0.1.4.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/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,24 +29,18 @@ class VsslIntEnum(VsslEnum, IntEnum):
|
|
|
29
29
|
"""IntEnum"""
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
pattern = r"\d+"
|
|
45
|
-
match = re.search(pattern, model)
|
|
46
|
-
if match:
|
|
47
|
-
number = int(match.group())
|
|
48
|
-
return number
|
|
49
|
-
return 1
|
|
32
|
+
#
|
|
33
|
+
# Zones IDs
|
|
34
|
+
#
|
|
35
|
+
# Moved here to help with circular imports
|
|
36
|
+
#
|
|
37
|
+
class ZoneIDs(VsslIntEnum):
|
|
38
|
+
ZONE_1 = 1
|
|
39
|
+
ZONE_2 = 2
|
|
40
|
+
ZONE_3 = 3
|
|
41
|
+
ZONE_4 = 4
|
|
42
|
+
ZONE_5 = 5
|
|
43
|
+
ZONE_6 = 6
|
|
50
44
|
|
|
51
45
|
|
|
52
46
|
""" JSON Structure
|
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:
|
|
@@ -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.4.dev1
|
|
4
4
|
Summary: Package for controlling VSSL amplifiers
|
|
5
5
|
Author-email: vsslctrl <vsslcontrolled@proton.me>
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -28,37 +28,43 @@ Requires-Dist: zeroconf ==0.132.2 ; extra == 'optional'
|
|
|
28
28
|
|
|
29
29
|
I am looking for testers with any VSSL amplifier models, please get in touch if you interested in helping.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Important
|
|
32
|
+
-----------
|
|
33
|
+
Only tested on a VSSL **A.3x** software version **p15305.016.3701**.
|
|
34
|
+
|
|
35
|
+
**Warning:** no *[VSSL Agent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start)* should be running on the same network. If you dont know what this is, then you can probably ignore this notice.
|
|
32
36
|
|
|
33
37
|
## TODOs
|
|
34
38
|
|
|
35
39
|
* Test on other models (hardware needed)
|
|
36
40
|
* Home Assistant integration. In progress, [here](https://github.com/vsslctrl/integration.home-assistant)
|
|
37
|
-
* Function scoping to supported feature / models
|
|
41
|
+
* ~~Function scoping to supported feature / models~~ (needs tests)
|
|
38
42
|
* Better test coverage
|
|
39
43
|
|
|
40
|
-
Important
|
|
41
|
-
-----------
|
|
42
|
-
**Warning:** no *[VSSL Agent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start)* should be running on the same network. If you dont know what this is, then you can probably ignore this notice.
|
|
43
|
-
|
|
44
|
-
|
|
45
44
|
Basic Usage
|
|
46
45
|
-----------
|
|
47
46
|
`vsslctrl` needs to be running inside a **[asyncio](https://docs.python.org/3/library/asyncio.html)** event loop.
|
|
48
47
|
|
|
49
48
|
```python
|
|
50
49
|
import asyncio
|
|
51
|
-
from vsslctrl import Vssl, Zone
|
|
50
|
+
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs
|
|
52
51
|
|
|
53
52
|
async def main():
|
|
54
53
|
|
|
55
54
|
# Represents a physical VSSL amplifier
|
|
56
55
|
vssl = Vssl()
|
|
57
56
|
|
|
57
|
+
"""Optional to init Vssl with a device model
|
|
58
|
+
|
|
59
|
+
vssl = Vssl(DeviceModels.A3X)
|
|
60
|
+
|
|
61
|
+
If no DeviceModels is passed, vsslctrl will default to the feature set of the X series amps
|
|
62
|
+
"""
|
|
63
|
+
|
|
58
64
|
# Add each you wish to control
|
|
59
|
-
zone1 = vssl.add_zone(
|
|
60
|
-
zone2 = vssl.add_zone(
|
|
61
|
-
zone3 = vssl.add_zone(
|
|
65
|
+
zone1 = vssl.add_zone(ZoneIDs.ZONE_1, '192.168.1.10')
|
|
66
|
+
zone2 = vssl.add_zone(ZoneIDs.ZONE_2, '192.168.1.11')
|
|
67
|
+
zone3 = vssl.add_zone(ZoneIDs.ZONE_3, '192.168.1.12')
|
|
62
68
|
#... up to 6 zones
|
|
63
69
|
|
|
64
70
|
# Connect and initiate zones.
|
|
@@ -110,7 +116,7 @@ print(zone_name)
|
|
|
110
116
|
| ---------------------- | ----------- | ----------- |
|
|
111
117
|
| `sw_version` | Software version | `str` readonly
|
|
112
118
|
| `serial` | Serial number | `str` readonly
|
|
113
|
-
| `
|
|
119
|
+
| `model` | Device Model | `int` readonly
|
|
114
120
|
| `reboot()` | Reboot all zones | `func` |
|
|
115
121
|
|
|
116
122
|
```python
|
|
@@ -152,7 +158,7 @@ vssl.settings.power.adaptive = True
|
|
|
152
158
|
|
|
153
159
|
| Property | Description | Type | Values |
|
|
154
160
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
155
|
-
| `id` | Zone number / ID | `int` readonly | `
|
|
161
|
+
| `id` | Zone number / ID | `int` readonly | `ZoneIDs`
|
|
156
162
|
| `host` | IP address | `str` readonly
|
|
157
163
|
| `volume` | Volume | `int` | `0...100`
|
|
158
164
|
| `volume_raise([step=1])` | Raise volume by `step` | `func` | step: `int` `1...100`
|
|
@@ -256,20 +262,20 @@ Working on A.3x but offically unsupported in x series amplifiers.
|
|
|
256
262
|
|
|
257
263
|
| Property | Description | Type | Values |
|
|
258
264
|
| ---------------------- | ----------- | ----------- |----------- |
|
|
259
|
-
| `source` | Zone ID of group master / source | `int` readonly | `
|
|
265
|
+
| `source` | Zone ID of group master / source | `int` readonly | `ZoneIDs`
|
|
260
266
|
| `is_master` | This zone is the group master | `bool` readonly
|
|
261
|
-
| `add_member()` | Add zone to group / create group | `func` | `
|
|
262
|
-
| `remove_member()` | Remove zone from group | `func` | `
|
|
267
|
+
| `add_member()` | Add zone to group / create group | `func` | `ZoneIDs`
|
|
268
|
+
| `remove_member()` | Remove zone from group | `func` | `ZoneIDs`
|
|
263
269
|
| `dissolve()` | Dissolve group / remove all members | `func` |
|
|
264
270
|
| `leave()` | Leave the group if a member | `func` |
|
|
265
271
|
|
|
266
272
|
```python
|
|
267
273
|
"""Examples"""
|
|
268
274
|
# Add group 2 to a group with zone 1 as master
|
|
269
|
-
zone1.group.add_member(
|
|
270
|
-
# Remove zone 2 from group
|
|
275
|
+
zone1.group.add_member(ZoneIDs.ZONE_2)
|
|
276
|
+
# Remove zone 2 from group
|
|
271
277
|
zone2.group.leave() # or
|
|
272
|
-
zone1.group.remove_member(
|
|
278
|
+
zone1.group.remove_member(ZoneIDs.ZONE_2)
|
|
273
279
|
# If zone 1 is a master, remove all members
|
|
274
280
|
zone1.group.dissolve()
|
|
275
281
|
```
|
|
@@ -371,6 +377,53 @@ EQ to be set in [decibel](https://en.wikipedia.org/wiki/Decibel) using a range `
|
|
|
371
377
|
zone1.settings.eq.khz1_db = -2
|
|
372
378
|
```
|
|
373
379
|
|
|
380
|
+
## Another (Lite) Way
|
|
381
|
+
|
|
382
|
+
If you perfer to not run the complete intergration, you can send basic HEX commands to the VSSL device using [Netcat](https://nc110.sourceforge.io/) (or ant network tool) on port `50002`.
|
|
383
|
+
|
|
384
|
+
| HEX | Description |
|
|
385
|
+
| ---------------------- | ----------- |
|
|
386
|
+
| `\x10\x05\x03\x0{Zone Number}\xff\x03` | Volume Up
|
|
387
|
+
| `\x10\x05\x03\x0{Zone Number}\xfe\x03` | Volume Down
|
|
388
|
+
| `\x10\x11\x02\x0{Zone Number}\x01` | Mute
|
|
389
|
+
| All commands can be found by looking [here](https://github.com/vsslctrl/vsslctrl/blob/2c43c2f2393b94bc0e062d2ab90144343eca16ef/vsslctrl/api_alpha.py) |
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
The `volume up` HEX command for `Zone 2` would be `\x10\x05\x03\x02\xff\x03`
|
|
393
|
+
|
|
394
|
+
Now send the raw HEX using Netcat to the device using this syntax:
|
|
395
|
+
|
|
396
|
+
`echo -e "{HEX Command}" | nc {IP Address} 50002`
|
|
397
|
+
|
|
398
|
+
For example to send `volume up` to `Zone 2`:
|
|
399
|
+
|
|
400
|
+
`echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002`
|
|
401
|
+
|
|
402
|
+
Home Assistant `Configuration.yaml` example:
|
|
403
|
+
|
|
404
|
+
```ymal
|
|
405
|
+
...
|
|
406
|
+
|
|
407
|
+
shell_command:
|
|
408
|
+
#Zone 1
|
|
409
|
+
vssl_zone_1_volume_up: 'echo -e "\x10\x05\x03\x01\xff\x03" | nc 192.168.1.10 50002'
|
|
410
|
+
vssl_zone_1_volume_down: 'echo -e "\x10\x05\x03\x01\xfe\x03" | nc 192.168.1.10 50002'
|
|
411
|
+
vssl_zone_1_mute: 'echo -e "\x10\x11\x02\x01\x01" | nc 192.168.1.10 50002'
|
|
412
|
+
vssl_zone_1_unmute: 'echo -e "\x10\x11\x02\x01\x00" | nc 192.168.1.10 50002'
|
|
413
|
+
#Zone 2
|
|
414
|
+
vssl_zone_2_volume_up: 'echo -e "\x10\x05\x03\x02\xff\x03" | nc 192.168.1.11 50002'
|
|
415
|
+
vssl_zone_2_volume_down: 'echo -e "\x10\x05\x03\x02\xfe\x03" | nc 192.168.1.11 50002'
|
|
416
|
+
vssl_zone_2_mute: 'echo -e "\x10\x11\x02\x02\x01" | nc 192.168.1.11 50002'
|
|
417
|
+
vssl_zone_2_unmute: 'echo -e "\x10\x11\x02\x02\x00" | nc 192.168.1.11 50002'
|
|
418
|
+
#Zone 3
|
|
419
|
+
vssl_zone_3_volume_up: 'echo -e "\x10\x05\x03\x03\xff\x03" | nc 192.168.1.12 50002'
|
|
420
|
+
vssl_zone_3_volume_down: 'echo -e "\x10\x05\x03\x03\xfe\x03" | nc 192.168.1.12 50002'
|
|
421
|
+
vssl_zone_3_mute: 'echo -e "\x10\x11\x02\x03\x01" | nc 192.168.1.12 50002'
|
|
422
|
+
vssl_zone_3_unmute: 'echo -e "\x10\x11\x02\x03\x00" | nc 192.168.1.12 50002'
|
|
423
|
+
|
|
424
|
+
...
|
|
425
|
+
```
|
|
426
|
+
|
|
374
427
|
## Credit
|
|
375
428
|
|
|
376
429
|
The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS app and their deprecated [vsslagent](https://vssl.gitbook.io/vssl-rest-api/getting-started/start).
|
|
@@ -392,4 +445,5 @@ The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS a
|
|
|
392
445
|
* REST API / Web App
|
|
393
446
|
* Save and recall EQ
|
|
394
447
|
* A.1(x) coverage i.e Bluetooth
|
|
448
|
+
* IR Control
|
|
395
449
|
|
|
@@ -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=BpDjKTopnWr9UXTPxOywri_2ndfV7m0MYiFk1nz4514,8998
|
|
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=S9Zp9aGHMztdRII1DTVuD4Spx6z9_AjwHaco9zjQ0OA,6245
|
|
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=Tnxk3hKf5G4pGDWvDlgH25DKXnESdX8S-3ecQOuXueU,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.4.dev1.dist-info/LICENSE,sha256=67Ekj8PIxjEvkPd7L3GYJ8dyvn1EC4sPM3q3FKtpN6w,1065
|
|
20
|
+
vsslctrl-0.1.4.dev1.dist-info/METADATA,sha256=XZq2ZiALLsD7l_D1HHsHZouN_x9ee0DA1BUgkz5AKOo,16311
|
|
21
|
+
vsslctrl-0.1.4.dev1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
22
|
+
vsslctrl-0.1.4.dev1.dist-info/top_level.txt,sha256=MRycO_RKageNX27UZ2bLJDMlfeWTQzUZlsd3hS-0u3Y,9
|
|
23
|
+
vsslctrl-0.1.4.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
|
|
File without changes
|