vsslctrl 0.1.6.dev1__tar.gz → 0.1.7.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.
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/PKG-INFO +1 -1
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/pyproject.toml +1 -1
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/api_alpha.py +17 -19
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/data_structure.py +51 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/device.py +3 -2
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/io.py +4 -4
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/settings.py +90 -1
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/zone.py +4 -2
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl.egg-info/PKG-INFO +1 -1
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/LICENSE +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/README.md +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/setup.cfg +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/tests/test_event_bus.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/tests/test_integration.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/tests/test_settings.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/tests/test_zone.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/__init__.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/api_base.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/api_bravo.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/core.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/decorators.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/discovery.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/event_bus.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/exceptions.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/group.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/track.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/transport.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl/utils.py +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl.egg-info/SOURCES.txt +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl.egg-info/dependency_links.txt +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl.egg-info/requires.txt +0 -0
- {vsslctrl-0.1.6.dev1 → vsslctrl-0.1.7.dev1}/vsslctrl.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vsslctrl"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7.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
|
|
98
|
+
# Status device extended
|
|
98
99
|
#
|
|
99
100
|
def request_action_00_0B(self):
|
|
100
|
-
self._log_debug("Requesting status
|
|
101
|
+
self._log_debug("Requesting device status extended")
|
|
101
102
|
self.send(bytearray([16, 0, 1, 11])) # HEX: 1000010B
|
|
102
103
|
|
|
103
104
|
#
|
|
@@ -746,28 +747,25 @@ class APIAlpha(APIBase):
|
|
|
746
747
|
|
|
747
748
|
#
|
|
748
749
|
# 00_0B
|
|
749
|
-
#
|
|
750
|
+
# Device Status Extended 0B
|
|
750
751
|
#
|
|
751
752
|
# {'IRMskL': '241', 'IRMskH': '255', 'BTSta': '0', 'Crs': '0', 'Fes': '0', 'Drk': '0'}
|
|
752
753
|
#
|
|
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
|
-
#
|
|
768
754
|
def response_action_00_0B(self, metadata: list):
|
|
769
755
|
self._log_debug(f"Received 0B Status: {metadata}")
|
|
770
|
-
|
|
756
|
+
|
|
757
|
+
# Bluetooth
|
|
758
|
+
if DeviceStatusExtendedExtKeys.BLUETOOTH_STATUS in metadata:
|
|
759
|
+
self.vssl.settings._set_property(
|
|
760
|
+
"bluetooth", int(metadata[DeviceStatusExtendedExtKeys.BLUETOOTH_STATUS])
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
# Subwoofer Crossover
|
|
764
|
+
if DeviceStatusExtendedExtKeys.SUBWOOFER_CROSSOVER in metadata:
|
|
765
|
+
self.zone.settings.subwoofer._set_property(
|
|
766
|
+
"crossover",
|
|
767
|
+
int(metadata[DeviceStatusExtendedExtKeys.SUBWOOFER_CROSSOVER]),
|
|
768
|
+
)
|
|
771
769
|
|
|
772
770
|
#
|
|
773
771
|
# 2A [42]
|
|
@@ -92,6 +92,57 @@ class DeviceStatusExtKeys:
|
|
|
92
92
|
return f"B{zone_id}Src"
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
"""
|
|
96
|
+
JSON Structure
|
|
97
|
+
|
|
98
|
+
DO NOT CHANGE - VSSL Defined
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
IRMskL / IRMskH:
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
BTSta:
|
|
105
|
+
This might represent the Bluetooth status, with "0" indicating that Bluetooth is currently not active or disconnected.
|
|
106
|
+
|
|
107
|
+
Crs:
|
|
108
|
+
It could stand for "Crossfade" and may represent a setting related to crossfading between audio tracks.
|
|
109
|
+
|
|
110
|
+
Fes:
|
|
111
|
+
This might stand for "Frequency" or "Filter Effect Setting," representing a parameter related to frequency or filtering effects.
|
|
112
|
+
|
|
113
|
+
Drk: ?
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
A3.x
|
|
117
|
+
{
|
|
118
|
+
'IRMskL': '241',
|
|
119
|
+
'IRMskH': '255',
|
|
120
|
+
'BTSta': '0',
|
|
121
|
+
'Crs': '0',
|
|
122
|
+
'Fes': '0',
|
|
123
|
+
'Drk': '0'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
A6.x
|
|
127
|
+
{
|
|
128
|
+
"IRMskL":"255",
|
|
129
|
+
"IRMskH":"255",
|
|
130
|
+
"BTSta":"0",
|
|
131
|
+
"Crs":"0",
|
|
132
|
+
"Fes":"0",
|
|
133
|
+
"Drk":"0"
|
|
134
|
+
'
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DeviceStatusExtendedExtKeys:
|
|
140
|
+
IR_HIGH = "IRMskH" # Guess - To Confirm
|
|
141
|
+
IR_LOW = "IRMskL" # Guess - To Confirm
|
|
142
|
+
BLUETOOTH_STATUS = "BTSta" # Guess - To Confirm
|
|
143
|
+
SUBWOOFER_CROSSOVER = "Crs" # Guess - To Confirm
|
|
144
|
+
|
|
145
|
+
|
|
95
146
|
"""
|
|
96
147
|
JSON Structure
|
|
97
148
|
|
|
@@ -36,6 +36,7 @@ class Features(VsslIntEnum):
|
|
|
36
36
|
GROUPING = 1000
|
|
37
37
|
BLUETOOTH = 1001
|
|
38
38
|
PARTY_MODE = 1002
|
|
39
|
+
SUBWOOFER_CROSSOVER = 1003
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
class Model:
|
|
@@ -65,7 +66,7 @@ class Models(VsslEnum):
|
|
|
65
66
|
"zones": SINGLE_ZONE,
|
|
66
67
|
"input_sources": INPUT_SOURCES_FOR_1_ZONE_DEVICE,
|
|
67
68
|
"analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE,
|
|
68
|
-
"features": [Features.BLUETOOTH],
|
|
69
|
+
"features": [Features.BLUETOOTH, Features.SUBWOOFER_CROSSOVER],
|
|
69
70
|
}
|
|
70
71
|
)
|
|
71
72
|
A3 = Model(
|
|
@@ -92,7 +93,7 @@ class Models(VsslEnum):
|
|
|
92
93
|
"zones": SINGLE_ZONE,
|
|
93
94
|
"input_sources": INPUT_SOURCES_FOR_1_ZONE_DEVICE,
|
|
94
95
|
"analog_output_sources": ANALOG_OUTPUT_SOURCES_FOR_1_ZONE_DEVICE,
|
|
95
|
-
"features": [Features.
|
|
96
|
+
"features": [Features.BLUETOOTH, Features.SUBWOOFER_CROSSOVER],
|
|
96
97
|
}
|
|
97
98
|
)
|
|
98
99
|
A3X = Model(
|
|
@@ -49,8 +49,8 @@ class InputRouter(ZoneDataClass):
|
|
|
49
49
|
def __init__(self, zone: "zone.Zone"):
|
|
50
50
|
self.zone = zone
|
|
51
51
|
|
|
52
|
-
self._priority = self.
|
|
53
|
-
self._source = self.
|
|
52
|
+
self._priority = self.DEFAULTS["priority"]
|
|
53
|
+
self._source = self.DEFAULTS["source"]
|
|
54
54
|
|
|
55
55
|
#
|
|
56
56
|
# Input Priority
|
|
@@ -138,7 +138,7 @@ class AnalogOutput(ZoneDataClass):
|
|
|
138
138
|
def __init__(self, zone: "zone.Zone"):
|
|
139
139
|
self.zone = zone
|
|
140
140
|
|
|
141
|
-
self._is_fixed_volume =
|
|
141
|
+
self._is_fixed_volume = self.DEFAULTS["is_fixed_volume"]
|
|
142
142
|
self._source = self.Sources(zone.id + 2)
|
|
143
143
|
|
|
144
144
|
#
|
|
@@ -199,7 +199,7 @@ class AnalogInput(ZoneDataClass):
|
|
|
199
199
|
self.zone = zone
|
|
200
200
|
|
|
201
201
|
self._name = f"Analog In {self.zone.id}"
|
|
202
|
-
self._fixed_gain =
|
|
202
|
+
self._fixed_gain = self.DEFAULTS["fixed_gain"]
|
|
203
203
|
|
|
204
204
|
#
|
|
205
205
|
# 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 = {
|
|
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
|
+
pass
|
|
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
|
#
|
|
@@ -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,59 @@ 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.api_alpha.request_action_crossover(freq)
|
|
739
|
+
|
|
740
|
+
def _set_crossover(self, freq: int):
|
|
741
|
+
freq = self._clamp_crossover(freq)
|
|
742
|
+
if self.crossover != freq:
|
|
743
|
+
self._crossover = freq
|
|
744
|
+
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.
|
|
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
|
|
411
|
+
def _request_status_extended(self):
|
|
410
412
|
self.api_alpha.request_action_00_0B()
|
|
411
413
|
return self
|
|
412
414
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|