vsslctrl 0.1.12.dev1__tar.gz → 0.1.14.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.12.dev1 → vsslctrl-0.1.14.dev1}/PKG-INFO +86 -30
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/README.md +84 -28
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/pyproject.toml +1 -1
- vsslctrl-0.1.14.dev1/tests/test_event_bus.py +206 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/__init__.py +1 -1
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/api_alpha.py +6 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/api_base.py +36 -2
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/api_bravo.py +6 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/core.py +20 -16
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/device.py +4 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/event_bus.py +19 -22
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/zone.py +8 -7
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl.egg-info/PKG-INFO +86 -30
- vsslctrl-0.1.12.dev1/tests/test_event_bus.py +0 -70
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/LICENSE +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/setup.cfg +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/tests/test_integration.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/tests/test_settings.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/tests/test_zone.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/data_structure.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/decorators.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/discovery.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/exceptions.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/group.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/io.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/settings.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/track.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/transport.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl/utils.py +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl.egg-info/SOURCES.txt +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl.egg-info/dependency_links.txt +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl.egg-info/requires.txt +0 -0
- {vsslctrl-0.1.12.dev1 → vsslctrl-0.1.14.dev1}/vsslctrl.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: vsslctrl
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14.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
|
|
@@ -76,7 +76,7 @@ async def main():
|
|
|
76
76
|
vssl = Vssl(DeviceModels.A1)
|
|
77
77
|
a1 = vssl.add_zone('192.168.1.10')
|
|
78
78
|
|
|
79
|
-
# Connect and
|
|
79
|
+
# Connect and initialise zone.
|
|
80
80
|
await vssl.initialise()
|
|
81
81
|
|
|
82
82
|
"""Control Examples"""
|
|
@@ -112,7 +112,7 @@ async def main():
|
|
|
112
112
|
zone3 = vssl.add_zone('192.168.1.12', ZoneIDs.ZONE_3)
|
|
113
113
|
#... up to 6 zones for A.6(x)
|
|
114
114
|
|
|
115
|
-
# Connect and
|
|
115
|
+
# Connect and initialise zones.
|
|
116
116
|
await vssl.initialise()
|
|
117
117
|
|
|
118
118
|
"""Control Examples"""
|
|
@@ -134,6 +134,77 @@ async def main():
|
|
|
134
134
|
asyncio.run(main())
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
### Device Discovery Helper
|
|
138
|
+
|
|
139
|
+
You can discover VSSL devices on the network using [mDNS](https://wikipedia.org/wiki/Multicast_DNS) / Bonjour if you have the [`zeroconf`](https://pypi.org/project/zeroconf/) package installed.
|
|
140
|
+
|
|
141
|
+
This uses airplay service string `_airplay._tcp.local.`, therefore airplay needs to available and will not work across VLANs without other provisions.
|
|
142
|
+
|
|
143
|
+
**Note:** This is designed to be a helper and its not recommended to be used for the initialization of the VSSL class.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import asyncio
|
|
147
|
+
from vsslctrl import Vssl
|
|
148
|
+
|
|
149
|
+
async def main():
|
|
150
|
+
|
|
151
|
+
print(await Vssl.discover())
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
{
|
|
155
|
+
'XXXXXXXXXXXX': [
|
|
156
|
+
{
|
|
157
|
+
'host': '192.168.168.25',
|
|
158
|
+
'name': 'Living Room',
|
|
159
|
+
'model': 'A1x',
|
|
160
|
+
'mac_addr': 'AA:BB:CC:DD:EE:FF',
|
|
161
|
+
'zone_id': '7',
|
|
162
|
+
'serial': 'XXXXXXXXXXXX'
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
asyncio.run(main())
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# API Functionality
|
|
174
|
+
|
|
175
|
+
Most functionality is achieved via `getters` and `setters` of the two main classes `Vssl`, `Zone`.
|
|
176
|
+
|
|
177
|
+
The classes will update the physical VSSL device when setting a property and once feedback has been received, the classes internal state will be updated. For example:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# Setting the zone name
|
|
181
|
+
zone1.settings.name = 'Living Room'
|
|
182
|
+
>>> 'Old Zone Name'
|
|
183
|
+
|
|
184
|
+
# Printing zone name
|
|
185
|
+
print(zone1.settings.name)
|
|
186
|
+
>>> 'Living Room'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Important** in the above example, `zone1.settings.name` won't be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events as below:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from vsslctrl.settings import ZoneSettings
|
|
193
|
+
# Setting the zone name and wait for feedback
|
|
194
|
+
future_name = vssl.event_bus.future(ZoneSettings.Events.NAME_CHANGE, zone1.id)
|
|
195
|
+
zone1.settings.name = 'Bathroom'
|
|
196
|
+
# Helper to await a future with timeout
|
|
197
|
+
new_name = await vssl.event_bus.wait_future(future_name)
|
|
198
|
+
# Printing zone name
|
|
199
|
+
print(new_name)
|
|
200
|
+
>>> 'Bathroom'
|
|
201
|
+
# or
|
|
202
|
+
print(zone1.settings.name)
|
|
203
|
+
>>> 'Bathroom'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
# API Reference
|
|
207
|
+
|
|
137
208
|
# `DeviceModels`
|
|
138
209
|
|
|
139
210
|
A device model has to be passed to VSSL so it knows internally what features are supported by the device.
|
|
@@ -150,35 +221,20 @@ This might be removed in the future if we can differentiate different models fro
|
|
|
150
221
|
|
|
151
222
|
# `ZoneIDs`
|
|
152
223
|
|
|
153
|
-
|
|
154
|
-
| ------------|--------- |
|
|
155
|
-
| `A1` | A.1(x) Only |
|
|
156
|
-
| `ZONE_1` | Zone 1 of A.3(x) and A.6(x) |
|
|
157
|
-
| `ZONE_2` | Zone 2 of A.3(x) and A.6(x) |
|
|
158
|
-
| `ZONE_3` | Zone 3 of A.3(x) and A.6(x) |
|
|
159
|
-
| `ZONE_4` | Zone 4 of A.6(x) |
|
|
160
|
-
| `ZONE_5` | Zone 5 of A.6(x) |
|
|
161
|
-
| `ZONE_6` | Zone 6 of A.6(x) |
|
|
224
|
+
A `ZoneIDs` must be passed to each `zone` you which to control and it must match the zone on the VSSL device.
|
|
162
225
|
|
|
226
|
+
If you are unsure of your `ZoneIDs` you could use the discovery helper to find out the correct mapping.
|
|
163
227
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
>>> None
|
|
174
|
-
|
|
175
|
-
# Printing zone name
|
|
176
|
-
zone_name = zone1.settings.name
|
|
177
|
-
print(zone_name)
|
|
178
|
-
>>> 'Living Room'
|
|
179
|
-
```
|
|
228
|
+
| Property | Description | A.1(x) | A.3(x) | A.6(x)
|
|
229
|
+
| ------------|--------- |-------- | -------- | -------- |
|
|
230
|
+
| `A1` | |✔️ | | |
|
|
231
|
+
| `ZONE_1` | Zone 1 | | ✔️| ✔️|
|
|
232
|
+
| `ZONE_2` | Zone 2 | | ✔️| ✔️|
|
|
233
|
+
| `ZONE_3` | Zone 3 | | ✔️| ✔️|
|
|
234
|
+
| `ZONE_4` | Zone 4 | | | ✔️|
|
|
235
|
+
| `ZONE_5` | Zone 5 | | | ✔️|
|
|
236
|
+
| `ZONE_6` | Zone 6 | | | ✔️|
|
|
180
237
|
|
|
181
|
-
**Important** in the above example, `zone1.settings.name` wont be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events.
|
|
182
238
|
|
|
183
239
|
# `Vssl`
|
|
184
240
|
|
|
@@ -55,7 +55,7 @@ async def main():
|
|
|
55
55
|
vssl = Vssl(DeviceModels.A1)
|
|
56
56
|
a1 = vssl.add_zone('192.168.1.10')
|
|
57
57
|
|
|
58
|
-
# Connect and
|
|
58
|
+
# Connect and initialise zone.
|
|
59
59
|
await vssl.initialise()
|
|
60
60
|
|
|
61
61
|
"""Control Examples"""
|
|
@@ -91,7 +91,7 @@ async def main():
|
|
|
91
91
|
zone3 = vssl.add_zone('192.168.1.12', ZoneIDs.ZONE_3)
|
|
92
92
|
#... up to 6 zones for A.6(x)
|
|
93
93
|
|
|
94
|
-
# Connect and
|
|
94
|
+
# Connect and initialise zones.
|
|
95
95
|
await vssl.initialise()
|
|
96
96
|
|
|
97
97
|
"""Control Examples"""
|
|
@@ -113,6 +113,77 @@ async def main():
|
|
|
113
113
|
asyncio.run(main())
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
### Device Discovery Helper
|
|
117
|
+
|
|
118
|
+
You can discover VSSL devices on the network using [mDNS](https://wikipedia.org/wiki/Multicast_DNS) / Bonjour if you have the [`zeroconf`](https://pypi.org/project/zeroconf/) package installed.
|
|
119
|
+
|
|
120
|
+
This uses airplay service string `_airplay._tcp.local.`, therefore airplay needs to available and will not work across VLANs without other provisions.
|
|
121
|
+
|
|
122
|
+
**Note:** This is designed to be a helper and its not recommended to be used for the initialization of the VSSL class.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import asyncio
|
|
126
|
+
from vsslctrl import Vssl
|
|
127
|
+
|
|
128
|
+
async def main():
|
|
129
|
+
|
|
130
|
+
print(await Vssl.discover())
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
{
|
|
134
|
+
'XXXXXXXXXXXX': [
|
|
135
|
+
{
|
|
136
|
+
'host': '192.168.168.25',
|
|
137
|
+
'name': 'Living Room',
|
|
138
|
+
'model': 'A1x',
|
|
139
|
+
'mac_addr': 'AA:BB:CC:DD:EE:FF',
|
|
140
|
+
'zone_id': '7',
|
|
141
|
+
'serial': 'XXXXXXXXXXXX'
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
asyncio.run(main())
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# API Functionality
|
|
153
|
+
|
|
154
|
+
Most functionality is achieved via `getters` and `setters` of the two main classes `Vssl`, `Zone`.
|
|
155
|
+
|
|
156
|
+
The classes will update the physical VSSL device when setting a property and once feedback has been received, the classes internal state will be updated. For example:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# Setting the zone name
|
|
160
|
+
zone1.settings.name = 'Living Room'
|
|
161
|
+
>>> 'Old Zone Name'
|
|
162
|
+
|
|
163
|
+
# Printing zone name
|
|
164
|
+
print(zone1.settings.name)
|
|
165
|
+
>>> 'Living Room'
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Important** in the above example, `zone1.settings.name` won't be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events as below:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from vsslctrl.settings import ZoneSettings
|
|
172
|
+
# Setting the zone name and wait for feedback
|
|
173
|
+
future_name = vssl.event_bus.future(ZoneSettings.Events.NAME_CHANGE, zone1.id)
|
|
174
|
+
zone1.settings.name = 'Bathroom'
|
|
175
|
+
# Helper to await a future with timeout
|
|
176
|
+
new_name = await vssl.event_bus.wait_future(future_name)
|
|
177
|
+
# Printing zone name
|
|
178
|
+
print(new_name)
|
|
179
|
+
>>> 'Bathroom'
|
|
180
|
+
# or
|
|
181
|
+
print(zone1.settings.name)
|
|
182
|
+
>>> 'Bathroom'
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
# API Reference
|
|
186
|
+
|
|
116
187
|
# `DeviceModels`
|
|
117
188
|
|
|
118
189
|
A device model has to be passed to VSSL so it knows internally what features are supported by the device.
|
|
@@ -129,35 +200,20 @@ This might be removed in the future if we can differentiate different models fro
|
|
|
129
200
|
|
|
130
201
|
# `ZoneIDs`
|
|
131
202
|
|
|
132
|
-
|
|
133
|
-
| ------------|--------- |
|
|
134
|
-
| `A1` | A.1(x) Only |
|
|
135
|
-
| `ZONE_1` | Zone 1 of A.3(x) and A.6(x) |
|
|
136
|
-
| `ZONE_2` | Zone 2 of A.3(x) and A.6(x) |
|
|
137
|
-
| `ZONE_3` | Zone 3 of A.3(x) and A.6(x) |
|
|
138
|
-
| `ZONE_4` | Zone 4 of A.6(x) |
|
|
139
|
-
| `ZONE_5` | Zone 5 of A.6(x) |
|
|
140
|
-
| `ZONE_6` | Zone 6 of A.6(x) |
|
|
203
|
+
A `ZoneIDs` must be passed to each `zone` you which to control and it must match the zone on the VSSL device.
|
|
141
204
|
|
|
205
|
+
If you are unsure of your `ZoneIDs` you could use the discovery helper to find out the correct mapping.
|
|
142
206
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
>>> None
|
|
153
|
-
|
|
154
|
-
# Printing zone name
|
|
155
|
-
zone_name = zone1.settings.name
|
|
156
|
-
print(zone_name)
|
|
157
|
-
>>> 'Living Room'
|
|
158
|
-
```
|
|
207
|
+
| Property | Description | A.1(x) | A.3(x) | A.6(x)
|
|
208
|
+
| ------------|--------- |-------- | -------- | -------- |
|
|
209
|
+
| `A1` | |✔️ | | |
|
|
210
|
+
| `ZONE_1` | Zone 1 | | ✔️| ✔️|
|
|
211
|
+
| `ZONE_2` | Zone 2 | | ✔️| ✔️|
|
|
212
|
+
| `ZONE_3` | Zone 3 | | ✔️| ✔️|
|
|
213
|
+
| `ZONE_4` | Zone 4 | | | ✔️|
|
|
214
|
+
| `ZONE_5` | Zone 5 | | | ✔️|
|
|
215
|
+
| `ZONE_6` | Zone 6 | | | ✔️|
|
|
159
216
|
|
|
160
|
-
**Important** in the above example, `zone1.settings.name` wont be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events.
|
|
161
217
|
|
|
162
218
|
# `Vssl`
|
|
163
219
|
|
|
@@ -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.14.dev1"
|
|
8
8
|
description = "Package for controlling VSSL's range of streaming amplifiers"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="vsslctrl", email="vsslcontrolled@proton.me" },
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import pytest
|
|
3
|
+
import pytest_asyncio
|
|
4
|
+
from vsslctrl.event_bus import EventBus
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestEventBus:
|
|
8
|
+
@pytest_asyncio.fixture(autouse=True)
|
|
9
|
+
async def event_bus(self):
|
|
10
|
+
bus = EventBus()
|
|
11
|
+
yield bus
|
|
12
|
+
bus.stop() # Ensure the event bus is stopped after each test
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
async def test_event_bus(self, event_bus):
|
|
16
|
+
# Instantiate the EventBus
|
|
17
|
+
# event_bus = EventBus()
|
|
18
|
+
|
|
19
|
+
async def publish_with_entity(self, entity=None):
|
|
20
|
+
# Define a callback function to be subscribed to the event
|
|
21
|
+
async def callback(data, *args, **kwargs):
|
|
22
|
+
callback.called = True
|
|
23
|
+
callback.data = data
|
|
24
|
+
|
|
25
|
+
# Subscribe the callback function to the event
|
|
26
|
+
event_type = "test_event"
|
|
27
|
+
event_bus.subscribe(event_type, callback, entity)
|
|
28
|
+
assert event_bus.subscribers.get(event_type) is not None
|
|
29
|
+
assert len(event_bus.subscribers.get(event_type)) == 1
|
|
30
|
+
assert event_bus.subscribers.get(event_type)[0][0] == callback
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
# Publish the event
|
|
34
|
+
event_data = "test_data"
|
|
35
|
+
event_bus.publish(event_type, data=event_data, entity=entity)
|
|
36
|
+
|
|
37
|
+
# Wait for a short time to allow the event to be processed
|
|
38
|
+
await asyncio.sleep(0.1)
|
|
39
|
+
|
|
40
|
+
# Assert that the callback function has been called
|
|
41
|
+
assert hasattr(callback, "called") and callback.called is True
|
|
42
|
+
assert getattr(callback, "data") == event_data
|
|
43
|
+
finally:
|
|
44
|
+
# Unsubscribe the callback function
|
|
45
|
+
event_bus.unsubscribe(event_type, callback)
|
|
46
|
+
# Ensure the callback function was not called after unsubscribing
|
|
47
|
+
assert len(event_bus.subscribers.get(event_type)) == 0
|
|
48
|
+
|
|
49
|
+
await publish_with_entity(None)
|
|
50
|
+
await publish_with_entity(1)
|
|
51
|
+
await publish_with_entity("test")
|
|
52
|
+
|
|
53
|
+
@pytest.mark.asyncio
|
|
54
|
+
async def test_event_bus_wildcard_event_type(self, event_bus):
|
|
55
|
+
# Define a callback function to be subscribed to the event
|
|
56
|
+
async def callback(data: int = 0, *args, **kwargs):
|
|
57
|
+
callback.data = data + 1
|
|
58
|
+
|
|
59
|
+
event_bus.subscribe(event_bus.WILDCARD, callback, 1)
|
|
60
|
+
# Ensure the callback function is subscribed
|
|
61
|
+
assert len(event_bus.subscribers.get(event_bus.WILDCARD)) == 1
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# Publish an event
|
|
65
|
+
event_bus.publish("random_event_1", 1, 0)
|
|
66
|
+
# Wait for a short time to allow the event to be processed
|
|
67
|
+
await asyncio.sleep(0.1)
|
|
68
|
+
assert getattr(callback, "data") == 1
|
|
69
|
+
|
|
70
|
+
# Publish an event
|
|
71
|
+
event_bus.publish("random_event_2.other", 1, 20)
|
|
72
|
+
# Wait for a short time to allow the event to be processed
|
|
73
|
+
await asyncio.sleep(0.1)
|
|
74
|
+
assert getattr(callback, "data") == 21
|
|
75
|
+
|
|
76
|
+
# Publish an event with different ID
|
|
77
|
+
event_bus.publish("random_event_2", 2, 30)
|
|
78
|
+
# Wait for a short time to allow the event to be processed
|
|
79
|
+
await asyncio.sleep(0.1)
|
|
80
|
+
assert getattr(callback, "data") == 21
|
|
81
|
+
|
|
82
|
+
finally:
|
|
83
|
+
# Unsubscribe the callback function
|
|
84
|
+
event_bus.unsubscribe(event_bus.WILDCARD, callback)
|
|
85
|
+
# Ensure the callback function was not called after unsubscribing
|
|
86
|
+
assert len(event_bus.subscribers.get(event_bus.WILDCARD)) == 0
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_event_bus_scoped_wildcard_event_type(self, event_bus):
|
|
90
|
+
# Define a callback function to be subscribed to the event
|
|
91
|
+
async def callback(data: int = 0, *args, **kwargs):
|
|
92
|
+
callback.data = data + 1
|
|
93
|
+
|
|
94
|
+
event_type = "zone.api." + event_bus.WILDCARD
|
|
95
|
+
|
|
96
|
+
event_bus.subscribe(event_type, callback, 1)
|
|
97
|
+
# Ensure the callback function is subscribed
|
|
98
|
+
assert len(event_bus.subscribers.get(event_type)) == 1
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Publish an event
|
|
102
|
+
event_bus.publish("zone.api.connected", 1, 0)
|
|
103
|
+
# Wait for a short time to allow the event to be processed
|
|
104
|
+
await asyncio.sleep(0.1)
|
|
105
|
+
assert getattr(callback, "data") == 1
|
|
106
|
+
|
|
107
|
+
# Publish an event
|
|
108
|
+
event_bus.publish("zone.api.disconnected", 1, 20)
|
|
109
|
+
# Wait for a short time to allow the event to be processed
|
|
110
|
+
await asyncio.sleep(0.1)
|
|
111
|
+
assert getattr(callback, "data") == 21
|
|
112
|
+
|
|
113
|
+
# Publish an different event
|
|
114
|
+
event_bus.publish("zone.api", 1, 30)
|
|
115
|
+
# Wait for a short time to allow the event to be processed
|
|
116
|
+
await asyncio.sleep(0.1)
|
|
117
|
+
assert getattr(callback, "data") == 21 # should be unchanged
|
|
118
|
+
|
|
119
|
+
# Publish an different event
|
|
120
|
+
event_bus.publish("zone", 1, 40)
|
|
121
|
+
# Wait for a short time to allow the event to be processed
|
|
122
|
+
await asyncio.sleep(0.1)
|
|
123
|
+
assert getattr(callback, "data") == 21 # should be unchanged
|
|
124
|
+
|
|
125
|
+
finally:
|
|
126
|
+
# Unsubscribe the callback function
|
|
127
|
+
event_bus.unsubscribe(event_type, callback)
|
|
128
|
+
# Ensure the callback function was not called after unsubscribing
|
|
129
|
+
assert len(event_bus.subscribers.get(event_type)) == 0
|
|
130
|
+
|
|
131
|
+
@pytest.mark.asyncio
|
|
132
|
+
async def test_event_bus_wildcard_entity(self, event_bus):
|
|
133
|
+
# Define a callback function to be subscribed to the event
|
|
134
|
+
async def callback(data: int = 0, *args, **kwargs):
|
|
135
|
+
callback.data = data + 1
|
|
136
|
+
|
|
137
|
+
test_event = "test_event"
|
|
138
|
+
|
|
139
|
+
event_bus.subscribe(test_event, callback, event_bus.WILDCARD)
|
|
140
|
+
# Ensure the callback function is subscribed
|
|
141
|
+
assert len(event_bus.subscribers.get(test_event)) == 1
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
# Publish an event
|
|
145
|
+
event_bus.publish(test_event, 1, 0)
|
|
146
|
+
await asyncio.sleep(0.1)
|
|
147
|
+
assert getattr(callback, "data") == 1
|
|
148
|
+
|
|
149
|
+
# Publish an event with different ID
|
|
150
|
+
event_bus.publish(test_event, 2, 20)
|
|
151
|
+
await asyncio.sleep(0.1)
|
|
152
|
+
assert getattr(callback, "data") == 21
|
|
153
|
+
|
|
154
|
+
# Publish an event with different ID
|
|
155
|
+
event_bus.publish("different_event", 2, 20)
|
|
156
|
+
await asyncio.sleep(0.1)
|
|
157
|
+
assert getattr(callback, "data") == 21
|
|
158
|
+
|
|
159
|
+
finally:
|
|
160
|
+
# Unsubscribe the callback function
|
|
161
|
+
event_bus.unsubscribe(test_event, callback)
|
|
162
|
+
# Ensure the callback function was not called after unsubscribing
|
|
163
|
+
assert len(event_bus.subscribers.get(test_event)) == 0
|
|
164
|
+
|
|
165
|
+
@pytest.mark.asyncio
|
|
166
|
+
async def test_event_bus_wildcard(self, event_bus):
|
|
167
|
+
# Define a callback function to be subscribed to the event
|
|
168
|
+
async def callback(data: int = 0, *args, **kwargs):
|
|
169
|
+
callback.data = data + 1
|
|
170
|
+
|
|
171
|
+
event_bus.subscribe(event_bus.WILDCARD, callback, event_bus.WILDCARD)
|
|
172
|
+
# Ensure the callback function is subscribed
|
|
173
|
+
assert len(event_bus.subscribers.get(event_bus.WILDCARD)) == 1
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
# Publish an event
|
|
177
|
+
event_bus.publish("random_event_1", 1, 0)
|
|
178
|
+
await asyncio.sleep(0.1)
|
|
179
|
+
assert getattr(callback, "data") == 1
|
|
180
|
+
|
|
181
|
+
# Publish an event with different ID
|
|
182
|
+
event_bus.publish("random_event_2", 2, 20)
|
|
183
|
+
await asyncio.sleep(0.1)
|
|
184
|
+
assert getattr(callback, "data") == 21
|
|
185
|
+
|
|
186
|
+
# Publish an event with different ID
|
|
187
|
+
event_bus.publish("random_event_3", 3, 30)
|
|
188
|
+
await asyncio.sleep(0.1)
|
|
189
|
+
assert getattr(callback, "data") == 31
|
|
190
|
+
finally:
|
|
191
|
+
# Unsubscribe the callback function
|
|
192
|
+
event_bus.unsubscribe(event_bus.WILDCARD, callback)
|
|
193
|
+
# Ensure the callback function was not called after unsubscribing
|
|
194
|
+
assert len(event_bus.subscribers.get(event_bus.WILDCARD)) == 0
|
|
195
|
+
|
|
196
|
+
@pytest.mark.asyncio
|
|
197
|
+
async def test_event_bus_future(self, event_bus):
|
|
198
|
+
# Subscribe the callback function to the event
|
|
199
|
+
event_type = "test_future_event"
|
|
200
|
+
future = event_bus.future(event_type, 1)
|
|
201
|
+
# Publish event
|
|
202
|
+
event_data = "test_data"
|
|
203
|
+
event_bus.publish(event_type, 1, event_data)
|
|
204
|
+
assert await future == event_data
|
|
205
|
+
# Make sure future is unsubscribed
|
|
206
|
+
assert len(event_bus.subscribers.get(event_type)) == 0
|
|
@@ -39,6 +39,12 @@ class APIAlpha(APIBase):
|
|
|
39
39
|
self.vssl = vssl_host
|
|
40
40
|
self.zone = zone
|
|
41
41
|
|
|
42
|
+
#
|
|
43
|
+
# Send event on event bus
|
|
44
|
+
#
|
|
45
|
+
def _event_publish(self, event_type, data=None):
|
|
46
|
+
self.zone._event_publish(event_type, (self.zone.host, self.TCP_PORT))
|
|
47
|
+
|
|
42
48
|
#
|
|
43
49
|
# Send keep alive
|
|
44
50
|
#
|
|
@@ -45,6 +45,17 @@ class APIBase(ABC):
|
|
|
45
45
|
|
|
46
46
|
FRIST_BYTE = 1
|
|
47
47
|
|
|
48
|
+
#
|
|
49
|
+
# API Events
|
|
50
|
+
#
|
|
51
|
+
class Events:
|
|
52
|
+
PREFIX = "zone.api."
|
|
53
|
+
CONNECTING = PREFIX + "connecting"
|
|
54
|
+
CONNECTED = PREFIX + "connected"
|
|
55
|
+
DISCONNECTING = PREFIX + "disconnecting"
|
|
56
|
+
DISCONNECTED = PREFIX + "disconnected"
|
|
57
|
+
RECONNECTING = PREFIX + "reconnecting"
|
|
58
|
+
|
|
48
59
|
def __init__(self, host, port):
|
|
49
60
|
self.host = host
|
|
50
61
|
self.port = port
|
|
@@ -89,6 +100,7 @@ class APIBase(ABC):
|
|
|
89
100
|
return self.connected
|
|
90
101
|
|
|
91
102
|
self._connecting = True
|
|
103
|
+
self._event_publish(self.Events.CONNECTING)
|
|
92
104
|
|
|
93
105
|
try:
|
|
94
106
|
self._log_debug(f"Attemping connection to {self.host}:{self.port}")
|
|
@@ -98,6 +110,7 @@ class APIBase(ABC):
|
|
|
98
110
|
|
|
99
111
|
# Connected
|
|
100
112
|
self.connection_event.set()
|
|
113
|
+
self._event_publish(self.Events.CONNECTED)
|
|
101
114
|
|
|
102
115
|
# cancel any reconnecting loops
|
|
103
116
|
self._cancel_keep_connected()
|
|
@@ -140,6 +153,7 @@ class APIBase(ABC):
|
|
|
140
153
|
@final
|
|
141
154
|
async def disconnect(self):
|
|
142
155
|
self._disconnecting = True
|
|
156
|
+
self._event_publish(self.Events.DISCONNECTING)
|
|
143
157
|
|
|
144
158
|
# cancel any reconnecting loops
|
|
145
159
|
self._cancel_keep_connected()
|
|
@@ -152,9 +166,14 @@ class APIBase(ABC):
|
|
|
152
166
|
|
|
153
167
|
if self._writer:
|
|
154
168
|
try:
|
|
169
|
+
# If there's unsent data in the buffer, wait_closed() might hang.
|
|
170
|
+
# Fix: Call await self._writer.drain() before closing to ensure all data is sent
|
|
171
|
+
await self._writer.drain()
|
|
172
|
+
|
|
155
173
|
# Writer hangs on disconnect sometimes
|
|
156
|
-
self._writer.
|
|
157
|
-
|
|
174
|
+
if not self._writer.is_closing():
|
|
175
|
+
self._writer.close()
|
|
176
|
+
await asyncio.wait_for(self._writer.wait_closed(), self.TIMEOUT)
|
|
158
177
|
|
|
159
178
|
except asyncio.CancelledError:
|
|
160
179
|
self._log_debug(f"writer close timeout")
|
|
@@ -168,6 +187,10 @@ class APIBase(ABC):
|
|
|
168
187
|
self._log_error(f"Unexpected error occurred while disconnecting: {e}")
|
|
169
188
|
# Handle unexpected errors
|
|
170
189
|
finally:
|
|
190
|
+
# If nothing works, you may need to forcefully close the socket:
|
|
191
|
+
# if hasattr(self._writer, 'transport'):
|
|
192
|
+
# self._writer.transport.close()
|
|
193
|
+
|
|
171
194
|
self._writer = None
|
|
172
195
|
|
|
173
196
|
self._reader = None
|
|
@@ -176,6 +199,8 @@ class APIBase(ABC):
|
|
|
176
199
|
|
|
177
200
|
self._disconnecting = False
|
|
178
201
|
|
|
202
|
+
self._event_publish(self.Events.DISCONNECTED)
|
|
203
|
+
|
|
179
204
|
return not self.connected
|
|
180
205
|
|
|
181
206
|
#
|
|
@@ -184,7 +209,9 @@ class APIBase(ABC):
|
|
|
184
209
|
@final
|
|
185
210
|
async def reconnect(self):
|
|
186
211
|
if not self._reconnecting and not self._is_keep_connected_running():
|
|
212
|
+
self._event_publish(self.Events.RECONNECTING)
|
|
187
213
|
await self.disconnect()
|
|
214
|
+
await asyncio.sleep(1)
|
|
188
215
|
self._keep_connected()
|
|
189
216
|
|
|
190
217
|
#
|
|
@@ -306,6 +333,13 @@ class APIBase(ABC):
|
|
|
306
333
|
async def _read_byte_stream(self):
|
|
307
334
|
pass
|
|
308
335
|
|
|
336
|
+
#
|
|
337
|
+
# Send event on event bus
|
|
338
|
+
#
|
|
339
|
+
@abstractmethod
|
|
340
|
+
def _event_publish(self, event_type, data=None):
|
|
341
|
+
pass
|
|
342
|
+
|
|
309
343
|
#
|
|
310
344
|
# Send a keep alive
|
|
311
345
|
#
|
|
@@ -23,6 +23,12 @@ class APIBravo(APIBase):
|
|
|
23
23
|
self.vssl = vssl_host
|
|
24
24
|
self.zone = zone
|
|
25
25
|
|
|
26
|
+
#
|
|
27
|
+
# Send event on event bus
|
|
28
|
+
#
|
|
29
|
+
def _event_publish(self, event_type, data=None):
|
|
30
|
+
self.zone._event_publish(event_type, (self.zone.host, self.TCP_PORT))
|
|
31
|
+
|
|
26
32
|
#
|
|
27
33
|
# Send keep alive
|
|
28
34
|
#
|
|
@@ -28,7 +28,6 @@ class Vssl:
|
|
|
28
28
|
MODEL_CHANGE = PREFIX + "model_changed"
|
|
29
29
|
SW_VERSION_CHANGE = PREFIX + "sw_version_changed"
|
|
30
30
|
SERIAL_CHANGE = PREFIX + "serial_changed"
|
|
31
|
-
ALL = EventBus.WILDCARD
|
|
32
31
|
|
|
33
32
|
def __init__(self, model: Models):
|
|
34
33
|
self.event_bus = EventBus()
|
|
@@ -50,7 +49,7 @@ class Vssl:
|
|
|
50
49
|
#
|
|
51
50
|
async def initialise(self, init_timeout: int = 10):
|
|
52
51
|
if len(self.zones) < 1:
|
|
53
|
-
raise VsslCtrlException("Add
|
|
52
|
+
raise VsslCtrlException("Add minimum one zone before initializing")
|
|
54
53
|
|
|
55
54
|
zones_to_init = self.zones.copy()
|
|
56
55
|
|
|
@@ -64,9 +63,9 @@ class Vssl:
|
|
|
64
63
|
await first_zone.initialise()
|
|
65
64
|
|
|
66
65
|
# Wait until we have some basic infomation
|
|
67
|
-
await self.event_bus.wait_future(future_serial)
|
|
68
|
-
await self.event_bus.wait_future(future_sw_version)
|
|
69
|
-
await self.event_bus.wait_future(future_name)
|
|
66
|
+
await self.event_bus.wait_future(future_serial, init_timeout)
|
|
67
|
+
await self.event_bus.wait_future(future_sw_version, init_timeout)
|
|
68
|
+
await self.event_bus.wait_future(future_name, init_timeout)
|
|
70
69
|
|
|
71
70
|
# Check we haven't added too many zones
|
|
72
71
|
if len(self.zones) > self.model.zone_count:
|
|
@@ -92,7 +91,7 @@ class Vssl:
|
|
|
92
91
|
raise
|
|
93
92
|
|
|
94
93
|
except asyncio.TimeoutError:
|
|
95
|
-
message = f"Timeout during VSSL initialization. Are any zones
|
|
94
|
+
message = f"Timeout during VSSL initialization. Are any zones available?"
|
|
96
95
|
self._log_critical(message)
|
|
97
96
|
await first_zone.disconnect()
|
|
98
97
|
raise VsslCtrlException(message)
|
|
@@ -114,17 +113,14 @@ class Vssl:
|
|
|
114
113
|
#
|
|
115
114
|
# Discover host on the network using zero_conf package
|
|
116
115
|
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
@staticmethod
|
|
117
|
+
async def discover(*args):
|
|
118
|
+
check_zeroconf_availability()
|
|
120
119
|
|
|
121
|
-
|
|
120
|
+
from .discovery import VsslDiscovery
|
|
122
121
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
except ZeroConfNotInstalled as e:
|
|
126
|
-
self._log_error(e)
|
|
127
|
-
raise
|
|
122
|
+
service = VsslDiscovery(*args)
|
|
123
|
+
return await service.discover()
|
|
128
124
|
|
|
129
125
|
#
|
|
130
126
|
# Update a property and fire an event
|
|
@@ -257,7 +253,7 @@ class Vssl:
|
|
|
257
253
|
return None
|
|
258
254
|
|
|
259
255
|
#
|
|
260
|
-
# Get a
|
|
256
|
+
# Get a zone that is connected
|
|
261
257
|
#
|
|
262
258
|
def get_connected_zone(self):
|
|
263
259
|
if self.zones:
|
|
@@ -265,6 +261,14 @@ class Vssl:
|
|
|
265
261
|
zone = self.zones[zone_id]
|
|
266
262
|
if zone.connected:
|
|
267
263
|
return zone
|
|
264
|
+
self._log_error("There are no connected zones.")
|
|
265
|
+
|
|
266
|
+
#
|
|
267
|
+
# Has a connected zone
|
|
268
|
+
#
|
|
269
|
+
@property
|
|
270
|
+
def connected(self):
|
|
271
|
+
return True if self.get_connected_zone() else False
|
|
268
272
|
|
|
269
273
|
#
|
|
270
274
|
# Get the device name
|
|
@@ -322,6 +322,10 @@ class Models(VsslEnum):
|
|
|
322
322
|
|
|
323
323
|
@classmethod
|
|
324
324
|
def get_model_by_name(cls, name):
|
|
325
|
+
# Return None immediately if input is None
|
|
326
|
+
if name is None:
|
|
327
|
+
return None
|
|
328
|
+
|
|
325
329
|
# Preprocess input name: convert to lowercase and remove dots
|
|
326
330
|
name_cleaned = name.lower().replace(".", "")
|
|
327
331
|
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import traceback
|
|
3
|
+
import fnmatch
|
|
3
4
|
from enum import IntEnum
|
|
4
5
|
from typing import Callable
|
|
5
6
|
from .exceptions import VsslCtrlException
|
|
6
7
|
from .decorators import logging_helpers
|
|
7
8
|
|
|
8
|
-
#
|
|
9
|
-
# Event Bus
|
|
10
|
-
#
|
|
11
|
-
|
|
12
9
|
|
|
13
10
|
@logging_helpers("EventBus:")
|
|
14
11
|
class EventBus:
|
|
@@ -23,6 +20,10 @@ class EventBus:
|
|
|
23
20
|
|
|
24
21
|
self.process = asyncio.create_task(self.process_events())
|
|
25
22
|
|
|
23
|
+
# Helper for wildcard matching, so we can use partial wildcards. e.g zone.api.connected
|
|
24
|
+
def _matches_pattern(self, event_type, pattern):
|
|
25
|
+
return fnmatch.fnmatch(event_type, pattern)
|
|
26
|
+
|
|
26
27
|
#
|
|
27
28
|
# Stop
|
|
28
29
|
#
|
|
@@ -67,7 +68,8 @@ class EventBus:
|
|
|
67
68
|
|
|
68
69
|
async def future_callback(data, *args):
|
|
69
70
|
nonlocal future
|
|
70
|
-
future.
|
|
71
|
+
if not future.done(): # Ensure the future is not already resolved
|
|
72
|
+
future.set_result(data)
|
|
71
73
|
|
|
72
74
|
self.subscribe(event_type, future_callback, entity, once=True)
|
|
73
75
|
|
|
@@ -115,12 +117,8 @@ class EventBus:
|
|
|
115
117
|
event_type = event_type.lower()
|
|
116
118
|
await self.event_queue.put((event_type, entity, data))
|
|
117
119
|
|
|
118
|
-
#
|
|
119
|
-
# Process Events
|
|
120
|
-
#
|
|
121
120
|
async def process_events(self):
|
|
122
121
|
self._log_debug(f"starting event processing")
|
|
123
|
-
|
|
124
122
|
self.running = True
|
|
125
123
|
while self.running:
|
|
126
124
|
try:
|
|
@@ -136,23 +134,22 @@ class EventBus:
|
|
|
136
134
|
message += str(data)
|
|
137
135
|
self._log_debug(message)
|
|
138
136
|
|
|
139
|
-
for
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
137
|
+
# Check for matching subscribers, including wildcards.
|
|
138
|
+
# e.g "zone.*", "zone.api.*"
|
|
139
|
+
matched_subscribers = []
|
|
140
|
+
for pattern in self.subscribers:
|
|
141
|
+
if self._matches_pattern(event_type, pattern):
|
|
142
|
+
matched_subscribers.extend(self.subscribers[pattern])
|
|
143
|
+
|
|
144
|
+
for callback, subscribed_entity, once in matched_subscribers:
|
|
145
|
+
if entity is None or subscribed_entity in {entity, self.WILDCARD}:
|
|
146
|
+
await callback(data, entity, event_type)
|
|
147
|
+
if once:
|
|
148
|
+
self.unsubscribe(pattern, callback)
|
|
151
149
|
|
|
152
150
|
except asyncio.CancelledError:
|
|
153
151
|
break
|
|
154
152
|
except Exception as e:
|
|
155
|
-
# Capture the traceback as a string
|
|
156
153
|
traceback_str = traceback.format_exc()
|
|
157
154
|
self._log_error(
|
|
158
155
|
f"exception occurred processing event: {e}\n{traceback_str}"
|
|
@@ -29,7 +29,6 @@ class Zone:
|
|
|
29
29
|
# Zone Events
|
|
30
30
|
#
|
|
31
31
|
class Events:
|
|
32
|
-
ALL = "*"
|
|
33
32
|
PREFIX = "zone."
|
|
34
33
|
INITIALISED = PREFIX + "initialised"
|
|
35
34
|
ID_RECEIVED = PREFIX + "id_received"
|
|
@@ -79,7 +78,7 @@ class Zone:
|
|
|
79
78
|
)
|
|
80
79
|
|
|
81
80
|
# Initialise
|
|
82
|
-
async def initialise(self):
|
|
81
|
+
async def initialise(self, init_timeout: int = 10):
|
|
83
82
|
# Data we require from the device
|
|
84
83
|
future_id = self.vssl.event_bus.future(self.Events.ID_RECEIVED, self.id)
|
|
85
84
|
future_serial = self.vssl.event_bus.future(self.Events.SERIAL_RECEIVED, self.id)
|
|
@@ -106,13 +105,15 @@ class Zone:
|
|
|
106
105
|
|
|
107
106
|
try:
|
|
108
107
|
# Wait for the ID, serial and name to be returned from the device
|
|
109
|
-
received_id = await self.vssl.event_bus.wait_future(future_id)
|
|
110
|
-
received_serial = await self.vssl.event_bus.wait_future(
|
|
111
|
-
|
|
108
|
+
received_id = await self.vssl.event_bus.wait_future(future_id, init_timeout)
|
|
109
|
+
received_serial = await self.vssl.event_bus.wait_future(
|
|
110
|
+
future_serial, init_timeout
|
|
111
|
+
)
|
|
112
|
+
await self.vssl.event_bus.wait_future(future_name, init_timeout)
|
|
112
113
|
except asyncio.TimeoutError:
|
|
113
|
-
message = f"Zone {self.id}: initialization timeout. Is the zone
|
|
114
|
+
message = f"Zone {self.id}: initialization timeout. Is the zone available?"
|
|
114
115
|
self._log_critical(message)
|
|
115
|
-
await
|
|
116
|
+
await self.disconnect()
|
|
116
117
|
raise ZoneError(message)
|
|
117
118
|
|
|
118
119
|
# Confirm the zone id is matches the returned zone ID
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: vsslctrl
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14.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
|
|
@@ -76,7 +76,7 @@ async def main():
|
|
|
76
76
|
vssl = Vssl(DeviceModels.A1)
|
|
77
77
|
a1 = vssl.add_zone('192.168.1.10')
|
|
78
78
|
|
|
79
|
-
# Connect and
|
|
79
|
+
# Connect and initialise zone.
|
|
80
80
|
await vssl.initialise()
|
|
81
81
|
|
|
82
82
|
"""Control Examples"""
|
|
@@ -112,7 +112,7 @@ async def main():
|
|
|
112
112
|
zone3 = vssl.add_zone('192.168.1.12', ZoneIDs.ZONE_3)
|
|
113
113
|
#... up to 6 zones for A.6(x)
|
|
114
114
|
|
|
115
|
-
# Connect and
|
|
115
|
+
# Connect and initialise zones.
|
|
116
116
|
await vssl.initialise()
|
|
117
117
|
|
|
118
118
|
"""Control Examples"""
|
|
@@ -134,6 +134,77 @@ async def main():
|
|
|
134
134
|
asyncio.run(main())
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
### Device Discovery Helper
|
|
138
|
+
|
|
139
|
+
You can discover VSSL devices on the network using [mDNS](https://wikipedia.org/wiki/Multicast_DNS) / Bonjour if you have the [`zeroconf`](https://pypi.org/project/zeroconf/) package installed.
|
|
140
|
+
|
|
141
|
+
This uses airplay service string `_airplay._tcp.local.`, therefore airplay needs to available and will not work across VLANs without other provisions.
|
|
142
|
+
|
|
143
|
+
**Note:** This is designed to be a helper and its not recommended to be used for the initialization of the VSSL class.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import asyncio
|
|
147
|
+
from vsslctrl import Vssl
|
|
148
|
+
|
|
149
|
+
async def main():
|
|
150
|
+
|
|
151
|
+
print(await Vssl.discover())
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
{
|
|
155
|
+
'XXXXXXXXXXXX': [
|
|
156
|
+
{
|
|
157
|
+
'host': '192.168.168.25',
|
|
158
|
+
'name': 'Living Room',
|
|
159
|
+
'model': 'A1x',
|
|
160
|
+
'mac_addr': 'AA:BB:CC:DD:EE:FF',
|
|
161
|
+
'zone_id': '7',
|
|
162
|
+
'serial': 'XXXXXXXXXXXX'
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
asyncio.run(main())
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# API Functionality
|
|
174
|
+
|
|
175
|
+
Most functionality is achieved via `getters` and `setters` of the two main classes `Vssl`, `Zone`.
|
|
176
|
+
|
|
177
|
+
The classes will update the physical VSSL device when setting a property and once feedback has been received, the classes internal state will be updated. For example:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# Setting the zone name
|
|
181
|
+
zone1.settings.name = 'Living Room'
|
|
182
|
+
>>> 'Old Zone Name'
|
|
183
|
+
|
|
184
|
+
# Printing zone name
|
|
185
|
+
print(zone1.settings.name)
|
|
186
|
+
>>> 'Living Room'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Important** in the above example, `zone1.settings.name` won't be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events as below:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from vsslctrl.settings import ZoneSettings
|
|
193
|
+
# Setting the zone name and wait for feedback
|
|
194
|
+
future_name = vssl.event_bus.future(ZoneSettings.Events.NAME_CHANGE, zone1.id)
|
|
195
|
+
zone1.settings.name = 'Bathroom'
|
|
196
|
+
# Helper to await a future with timeout
|
|
197
|
+
new_name = await vssl.event_bus.wait_future(future_name)
|
|
198
|
+
# Printing zone name
|
|
199
|
+
print(new_name)
|
|
200
|
+
>>> 'Bathroom'
|
|
201
|
+
# or
|
|
202
|
+
print(zone1.settings.name)
|
|
203
|
+
>>> 'Bathroom'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
# API Reference
|
|
207
|
+
|
|
137
208
|
# `DeviceModels`
|
|
138
209
|
|
|
139
210
|
A device model has to be passed to VSSL so it knows internally what features are supported by the device.
|
|
@@ -150,35 +221,20 @@ This might be removed in the future if we can differentiate different models fro
|
|
|
150
221
|
|
|
151
222
|
# `ZoneIDs`
|
|
152
223
|
|
|
153
|
-
|
|
154
|
-
| ------------|--------- |
|
|
155
|
-
| `A1` | A.1(x) Only |
|
|
156
|
-
| `ZONE_1` | Zone 1 of A.3(x) and A.6(x) |
|
|
157
|
-
| `ZONE_2` | Zone 2 of A.3(x) and A.6(x) |
|
|
158
|
-
| `ZONE_3` | Zone 3 of A.3(x) and A.6(x) |
|
|
159
|
-
| `ZONE_4` | Zone 4 of A.6(x) |
|
|
160
|
-
| `ZONE_5` | Zone 5 of A.6(x) |
|
|
161
|
-
| `ZONE_6` | Zone 6 of A.6(x) |
|
|
224
|
+
A `ZoneIDs` must be passed to each `zone` you which to control and it must match the zone on the VSSL device.
|
|
162
225
|
|
|
226
|
+
If you are unsure of your `ZoneIDs` you could use the discovery helper to find out the correct mapping.
|
|
163
227
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
>>> None
|
|
174
|
-
|
|
175
|
-
# Printing zone name
|
|
176
|
-
zone_name = zone1.settings.name
|
|
177
|
-
print(zone_name)
|
|
178
|
-
>>> 'Living Room'
|
|
179
|
-
```
|
|
228
|
+
| Property | Description | A.1(x) | A.3(x) | A.6(x)
|
|
229
|
+
| ------------|--------- |-------- | -------- | -------- |
|
|
230
|
+
| `A1` | |✔️ | | |
|
|
231
|
+
| `ZONE_1` | Zone 1 | | ✔️| ✔️|
|
|
232
|
+
| `ZONE_2` | Zone 2 | | ✔️| ✔️|
|
|
233
|
+
| `ZONE_3` | Zone 3 | | ✔️| ✔️|
|
|
234
|
+
| `ZONE_4` | Zone 4 | | | ✔️|
|
|
235
|
+
| `ZONE_5` | Zone 5 | | | ✔️|
|
|
236
|
+
| `ZONE_6` | Zone 6 | | | ✔️|
|
|
180
237
|
|
|
181
|
-
**Important** in the above example, `zone1.settings.name` wont be set to its new value until after the VSSL device has changed the name and the `Zone` class has received confirmation feedback. If you need to wait for the value change, you can await a `[property_name]_CHANGE` events.
|
|
182
238
|
|
|
183
239
|
# `Vssl`
|
|
184
240
|
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import pytest
|
|
3
|
-
import pytest_asyncio
|
|
4
|
-
from vsslctrl.event_bus import EventBus
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestEventBus:
|
|
8
|
-
|
|
9
|
-
@pytest_asyncio.fixture(autouse=True)
|
|
10
|
-
async def event_bus(self):
|
|
11
|
-
bus = EventBus()
|
|
12
|
-
yield bus
|
|
13
|
-
bus.stop() # Ensure the event bus is stopped after each test
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@pytest.mark.asyncio
|
|
17
|
-
async def test_event_bus(self, event_bus):
|
|
18
|
-
# Instantiate the EventBus
|
|
19
|
-
#event_bus = EventBus()
|
|
20
|
-
|
|
21
|
-
async def publish_with_entity(self, entity = None):
|
|
22
|
-
|
|
23
|
-
# Define a callback function to be subscribed to the event
|
|
24
|
-
async def callback(data, *args, **kwargs):
|
|
25
|
-
callback.called = True
|
|
26
|
-
callback.data = data
|
|
27
|
-
|
|
28
|
-
# Subscribe the callback function to the event
|
|
29
|
-
event_type = "test_event"
|
|
30
|
-
event_bus.subscribe(event_type, callback, entity)
|
|
31
|
-
assert event_bus.subscribers.get(event_type) is not None
|
|
32
|
-
assert len(event_bus.subscribers.get(event_type)) == 1
|
|
33
|
-
assert event_bus.subscribers.get(event_type)[0][0] == callback
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
# Publish the event
|
|
38
|
-
event_data = "test_data"
|
|
39
|
-
event_bus.publish(event_type, data=event_data, entity=entity)
|
|
40
|
-
|
|
41
|
-
# Wait for a short time to allow the event to be processed
|
|
42
|
-
await asyncio.sleep(0.1)
|
|
43
|
-
|
|
44
|
-
# Assert that the callback function has been called
|
|
45
|
-
assert hasattr(callback, 'called') and callback.called is True
|
|
46
|
-
assert getattr(callback, 'data') == event_data
|
|
47
|
-
finally:
|
|
48
|
-
# Unsubscribe the callback function
|
|
49
|
-
event_bus.unsubscribe(event_type, callback)
|
|
50
|
-
# Ensure the callback function was not called after unsubscribing
|
|
51
|
-
assert len(event_bus.subscribers.get(event_type)) == 0
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await publish_with_entity(None)
|
|
55
|
-
await publish_with_entity(1)
|
|
56
|
-
await publish_with_entity('tet')
|
|
57
|
-
|
|
58
|
-
@pytest.mark.asyncio
|
|
59
|
-
async def test_event_bus_future(self, event_bus):
|
|
60
|
-
|
|
61
|
-
# Subscribe the callback function to the event
|
|
62
|
-
event_type = "test_event"
|
|
63
|
-
future = event_bus.future(event_type, 1)
|
|
64
|
-
event_data = "test_data"
|
|
65
|
-
event_bus.publish(event_type, data=event_data, entity=1)
|
|
66
|
-
data = await future
|
|
67
|
-
assert data == event_data
|
|
68
|
-
assert len(event_bus.subscribers.get(event_type)) == 0
|
|
69
|
-
|
|
70
|
-
|
|
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
|