velbus-aio 2025.5.0__py3-none-any.whl → 2025.8.0__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.
Potentially problematic release.
This version of velbus-aio might be problematic. Click here for more details.
- {velbus_aio-2025.5.0.dist-info → velbus_aio-2025.8.0.dist-info}/METADATA +1 -1
- {velbus_aio-2025.5.0.dist-info → velbus_aio-2025.8.0.dist-info}/RECORD +11 -11
- {velbus_aio-2025.5.0.dist-info → velbus_aio-2025.8.0.dist-info}/WHEEL +1 -1
- velbusaio/controller.py +11 -8
- velbusaio/handler.py +113 -85
- velbusaio/message.py +7 -1
- velbusaio/messages/module_type.py +1 -0
- velbusaio/module.py +33 -11
- velbusaio/protocol.py +29 -6
- {velbus_aio-2025.5.0.dist-info → velbus_aio-2025.8.0.dist-info}/licenses/LICENSE +0 -0
- {velbus_aio-2025.5.0.dist-info → velbus_aio-2025.8.0.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
velbus_aio-2025.
|
|
1
|
+
velbus_aio-2025.8.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
2
2
|
velbusaio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
velbusaio/channels.py,sha256=pR-y7Res_MAPSFEM62eLx844HU0EjX7SyyEpJT4YU74,23556
|
|
4
4
|
velbusaio/command_registry.py,sha256=nlx4__0ASr5WurOYLC-HEqswlg_qm3Y5trs4V8FhnvU,5350
|
|
5
5
|
velbusaio/const.py,sha256=aHClMaMS6NINTCrjeRwLbVzdvS91VgFKM8j61ks5tn0,1862
|
|
6
|
-
velbusaio/controller.py,sha256=
|
|
6
|
+
velbusaio/controller.py,sha256=zvkd8JWHZEKk9rg0GOAGRR3OS97B2ugQkeKQh-ucaFA,9111
|
|
7
7
|
velbusaio/discovery.py,sha256=Px6qoZl4QhF17aMz6JxstCORBpLzZGWEK9h4Vyvg57o,1649
|
|
8
8
|
velbusaio/exceptions.py,sha256=FHkXaM3dK5Gkk-QGAf9dLE3FPlCU2FRZWUyY-4KRNnA,515
|
|
9
|
-
velbusaio/handler.py,sha256=
|
|
9
|
+
velbusaio/handler.py,sha256=7F5E3vkkptk6alOTg0dIpyF5Uqd6xpzXoJMXkmR9r58,13126
|
|
10
10
|
velbusaio/helpers.py,sha256=iqpoereRH4JY5WAkozIqWvXWyRmhko-J-UGXDylFyEM,2537
|
|
11
|
-
velbusaio/message.py,sha256=
|
|
12
|
-
velbusaio/module.py,sha256=
|
|
13
|
-
velbusaio/protocol.py,sha256=
|
|
11
|
+
velbusaio/message.py,sha256=Qzdx6Qrk-DptRnkdfirp5jEKA4hoANtPHfAxvWHzpvU,5305
|
|
12
|
+
velbusaio/module.py,sha256=kVaOp8ZLTSA4OMyTxKx4fJDAQw39teIxdtLagWsrCdc,38975
|
|
13
|
+
velbusaio/protocol.py,sha256=59Qdw2bZ9wL4_XmxvqRD8R1LJoWfrwRRaCOM3KGa0x0,8808
|
|
14
14
|
velbusaio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
velbusaio/raw_message.py,sha256=ODoTPVAvyXXQSMXxtQO1U5OxcoQ4W8JJN5fotEDGSTo,4690
|
|
16
16
|
velbusaio/util.py,sha256=FW6YCiPYWOCgqHDs8-LbzME0h81mqftYVG0qqZ-oo7Y,1568
|
|
@@ -54,7 +54,7 @@ velbusaio/messages/memory_dump_request.py,sha256=xrPsdpygD9DUF3lp-BzJacuq1dNkqz2
|
|
|
54
54
|
velbusaio/messages/module_status.py,sha256=Y2OQUzijtA27snTEVHlyoOrcaLQbSq_3KHKAVDLGR_o,6662
|
|
55
55
|
velbusaio/messages/module_status_request.py,sha256=s3h8F6Dpmjcb4hqDQKIzdOgocW1F_st40cctcW7WmQ4,961
|
|
56
56
|
velbusaio/messages/module_subtype.py,sha256=KG-OC0eW-ZRnP1P8EWmcay6O-no6Mn2cklEjokaIW9M,1487
|
|
57
|
-
velbusaio/messages/module_type.py,sha256=
|
|
57
|
+
velbusaio/messages/module_type.py,sha256=l5CCLWdsYPxCB4yXrtnJFU9nbKUWTHgIUj-8d2yNRBQ,4373
|
|
58
58
|
velbusaio/messages/module_type_request.py,sha256=fntMYCF1WVwSseCy3zRoZQH6JeGz8JDfTkwHC3AvcB4,754
|
|
59
59
|
velbusaio/messages/psu_load.py,sha256=001KIz84CbnC54gHxhfAiNbcEMRpVUBzDwQ7T-yxY8s,1232
|
|
60
60
|
velbusaio/messages/psu_values.py,sha256=Yd_ndU8yIei7CoL2NV6FHCefO5h_gzRsPNUYxGUGP0o,1300
|
|
@@ -184,7 +184,7 @@ velbusaio/module_spec/5C.json,sha256=yw_XlccuFp-buRL_NXThwQLwblVRa0diLpUDm9Exrg0
|
|
|
184
184
|
velbusaio/module_spec/5F.json,sha256=ebSwLMUW5A4LCRGOrF-vwxSVvrWZMNUWz10Xrmz7SwM,1259
|
|
185
185
|
velbusaio/module_spec/60.json,sha256=Teih3jAybl5AELjY3gFbZrFN0RIMSU1yuvdsYZlMvTo,74
|
|
186
186
|
velbusaio/module_spec/broadcast.json,sha256=L2Sc9FhZ7NOuu7K5g5jAhvUQGDP6a2CKrg-uWh_v-sA,2250
|
|
187
|
-
velbus_aio-2025.
|
|
188
|
-
velbus_aio-2025.
|
|
189
|
-
velbus_aio-2025.
|
|
190
|
-
velbus_aio-2025.
|
|
187
|
+
velbus_aio-2025.8.0.dist-info/METADATA,sha256=xIAwPhzJOI2M9SgDscvXyYKzwdXuR9YShcwDVYfJ3GM,4562
|
|
188
|
+
velbus_aio-2025.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
189
|
+
velbus_aio-2025.8.0.dist-info/top_level.txt,sha256=W0-lSOwD23mm8FqaIe9vY20fKicBMIdUVjF-zmfxRnY,15
|
|
190
|
+
velbus_aio-2025.8.0.dist-info/RECORD,,
|
velbusaio/controller.py
CHANGED
|
@@ -17,7 +17,6 @@ from velbusaio.channels import (
|
|
|
17
17
|
Blind,
|
|
18
18
|
Button,
|
|
19
19
|
ButtonCounter,
|
|
20
|
-
Channel,
|
|
21
20
|
Dimmer,
|
|
22
21
|
EdgeLit,
|
|
23
22
|
LightSensor,
|
|
@@ -61,7 +60,7 @@ class Velbus:
|
|
|
61
60
|
self._closing = False
|
|
62
61
|
self._auto_reconnect = True
|
|
63
62
|
|
|
64
|
-
self.
|
|
63
|
+
self._destination = dsn
|
|
65
64
|
self._handler = PacketHandler(self, one_address)
|
|
66
65
|
self._modules: dict[int, Module] = {}
|
|
67
66
|
self._submodules: list[int] = []
|
|
@@ -146,12 +145,12 @@ class Velbus:
|
|
|
146
145
|
"""Connect to the bus and load all the data."""
|
|
147
146
|
await self._handler.read_protocol_data()
|
|
148
147
|
# connect to the bus
|
|
149
|
-
if ":" in self.
|
|
148
|
+
if ":" in self._destination:
|
|
150
149
|
# tcp/ip combination
|
|
151
|
-
if not re.search(r"^[A-Za-z0-9+.\-]+://", self.
|
|
150
|
+
if not re.search(r"^[A-Za-z0-9+.\-]+://", self._destination):
|
|
152
151
|
# if no scheme, then add the tcp://
|
|
153
|
-
self.
|
|
154
|
-
parts = urlparse(self.
|
|
152
|
+
self._destination = f"tcp://{self._destination}"
|
|
153
|
+
parts = urlparse(self._destination)
|
|
155
154
|
if parts.scheme == "tls":
|
|
156
155
|
ctx = ssl._create_unverified_context()
|
|
157
156
|
else:
|
|
@@ -176,7 +175,7 @@ class Velbus:
|
|
|
176
175
|
await serial_asyncio_fast.create_serial_connection(
|
|
177
176
|
asyncio.get_event_loop(),
|
|
178
177
|
lambda: self._protocol,
|
|
179
|
-
url=self.
|
|
178
|
+
url=self._destination,
|
|
180
179
|
baudrate=38400,
|
|
181
180
|
bytesize=serial.EIGHTBITS,
|
|
182
181
|
parity=serial.PARITY_NONE,
|
|
@@ -190,7 +189,7 @@ class Velbus:
|
|
|
190
189
|
|
|
191
190
|
async def start(self) -> None:
|
|
192
191
|
# if auth is required send the auth key
|
|
193
|
-
parts = urlparse(self.
|
|
192
|
+
parts = urlparse(self._destination)
|
|
194
193
|
if parts.username:
|
|
195
194
|
await self._protocol.write_auth_key(parts.username)
|
|
196
195
|
# scan the bus
|
|
@@ -277,3 +276,7 @@ class Velbus:
|
|
|
277
276
|
await self.send(SetRealtimeClock(wday=lclt[6], hour=lclt[3], min=lclt[4]))
|
|
278
277
|
await self.send(SetDate(day=lclt[2], mon=lclt[1], year=lclt[0]))
|
|
279
278
|
await self.send(SetDaylightSaving(ds=not lclt[8]))
|
|
279
|
+
|
|
280
|
+
async def wait_on_all_messages_sent_async(self) -> None:
|
|
281
|
+
"""Wait for all messages to be sent."""
|
|
282
|
+
await self._protocol.wait_on_all_messages_sent_async()
|
velbusaio/handler.py
CHANGED
|
@@ -11,9 +11,9 @@ import json
|
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
13
|
import pathlib
|
|
14
|
-
import pprint
|
|
15
14
|
import sys
|
|
16
|
-
|
|
15
|
+
import time
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
17
|
|
|
18
18
|
from aiofile import async_open
|
|
19
19
|
|
|
@@ -23,10 +23,9 @@ from velbusaio.const import (
|
|
|
23
23
|
SCAN_MODULEINFO_TIMEOUT_INTERVAL,
|
|
24
24
|
SCAN_MODULETYPE_TIMEOUT,
|
|
25
25
|
)
|
|
26
|
-
from velbusaio.helpers import h2, keys_exists
|
|
27
|
-
from velbusaio.message import Message
|
|
28
26
|
from velbusaio.messages.module_subtype import ModuleSubTypeMessage
|
|
29
27
|
from velbusaio.messages.module_type import ModuleType2Message, ModuleTypeMessage
|
|
28
|
+
from velbusaio.module import Module
|
|
30
29
|
from velbusaio.raw_message import RawMessage
|
|
31
30
|
|
|
32
31
|
if TYPE_CHECKING:
|
|
@@ -35,7 +34,7 @@ if TYPE_CHECKING:
|
|
|
35
34
|
|
|
36
35
|
class PacketHandler:
|
|
37
36
|
"""
|
|
38
|
-
The
|
|
37
|
+
The PacketHandler class
|
|
39
38
|
"""
|
|
40
39
|
|
|
41
40
|
def __init__(
|
|
@@ -49,9 +48,11 @@ class PacketHandler:
|
|
|
49
48
|
self._one_address = one_address
|
|
50
49
|
self._typeResponseReceived = asyncio.Event()
|
|
51
50
|
self._scanLock = asyncio.Lock()
|
|
51
|
+
self._fullScanLock = asyncio.Lock()
|
|
52
52
|
self._modulescan_address = 0
|
|
53
53
|
self._scan_complete = False
|
|
54
54
|
self._scan_delay_msec = 0
|
|
55
|
+
self.__scan_found_addresses: dict[int, ModuleTypeMessage | None] | None = None
|
|
55
56
|
|
|
56
57
|
async def read_protocol_data(self):
|
|
57
58
|
if sys.version_info >= (3, 13):
|
|
@@ -85,84 +86,123 @@ class PacketHandler:
|
|
|
85
86
|
return False
|
|
86
87
|
|
|
87
88
|
async def scan(self, reload_cache: bool = False) -> None:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
start_address = 1
|
|
90
|
+
max_address = 254 + 1
|
|
91
|
+
if self._one_address is not None:
|
|
92
|
+
start_address = self._one_address
|
|
93
|
+
max_address = self._one_address + 1
|
|
94
|
+
self._log.info(
|
|
95
|
+
f"Scanning only one address {self._one_address} ({self._one_address:#02x})"
|
|
96
|
+
)
|
|
97
|
+
|
|
96
98
|
self._log.info("Start module scan")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
async with self._fullScanLock:
|
|
100
|
+
start_time = time.perf_counter()
|
|
101
|
+
self._scan_complete = False
|
|
102
|
+
|
|
103
|
+
self._log.debug("Waiting for Velbus bus to be ready to scan...")
|
|
104
|
+
await self._velbus.wait_on_all_messages_sent_async() # don't start a scan while messages are still in the queue
|
|
105
|
+
self._log.debug("Velbus bus is ready to scan!")
|
|
106
|
+
|
|
107
|
+
self._log.info("Sending scan type requests to all addresses...")
|
|
108
|
+
start_scan_time = time.perf_counter()
|
|
109
|
+
self.__scan_found_addresses = {}
|
|
110
|
+
for address in range(start_address, max_address):
|
|
111
|
+
cfile = pathlib.Path(f"{self._velbus.get_cache_dir()}/{address}.json")
|
|
112
|
+
if reload_cache and os.path.isfile(cfile):
|
|
104
113
|
self._log.info(
|
|
105
|
-
f"
|
|
114
|
+
f"Reloading cache for address {address} ({address:#02x})"
|
|
115
|
+
)
|
|
116
|
+
os.remove(cfile)
|
|
117
|
+
|
|
118
|
+
self.__scan_found_addresses[address] = None
|
|
119
|
+
async with self._scanLock:
|
|
120
|
+
await self._velbus.sendTypeRequestMessage(address)
|
|
121
|
+
|
|
122
|
+
await self._velbus.wait_on_all_messages_sent_async()
|
|
123
|
+
scan_time = time.perf_counter() - start_scan_time
|
|
124
|
+
self._log.info(
|
|
125
|
+
f"Sent scan type requests to all addresses in {scan_time:.2f}. Going to wait for responses..."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
await asyncio.sleep(SCAN_MODULETYPE_TIMEOUT / 1000) # wait for responses
|
|
129
|
+
|
|
130
|
+
self._log.info(
|
|
131
|
+
"Waiting for responses done. Going to check for responses..."
|
|
132
|
+
)
|
|
133
|
+
for address in range(start_address, max_address):
|
|
134
|
+
start_module_scan = time.perf_counter()
|
|
135
|
+
module_type_message: ModuleTypeMessage | None = (
|
|
136
|
+
self.__scan_found_addresses[address]
|
|
137
|
+
)
|
|
138
|
+
module: Module | None = None
|
|
139
|
+
if module_type_message is None:
|
|
140
|
+
self._log.debug(
|
|
141
|
+
f"No module found at address {address} ({address:#02x}). Skipping it."
|
|
106
142
|
)
|
|
107
143
|
continue
|
|
108
|
-
self._log.info(f"Starting handling scan {address}")
|
|
109
|
-
module = self._velbus.get_module(address)
|
|
110
144
|
|
|
111
|
-
if self._one_address is not None and address != int(self._one_address):
|
|
112
145
|
self._log.info(
|
|
113
|
-
f"
|
|
146
|
+
f"Found module at address {address} ({address:#02x}): {module_type_message.module_type_name()}"
|
|
114
147
|
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
148
|
+
# cache_file = pathlib.Path(f"{self._velbus.get_cache_dir()}/{address}.json")
|
|
149
|
+
# TODO: check if cached file module type is the same?
|
|
150
|
+
await self._handle_module_type(module_type_message)
|
|
151
|
+
async with self._scanLock:
|
|
152
|
+
module = self._velbus.get_module(address)
|
|
153
|
+
|
|
154
|
+
if module is None:
|
|
155
|
+
self._log.info(
|
|
156
|
+
f"Module at address {address} ({address:#02x}) could not be loaded. Skipping it."
|
|
157
|
+
)
|
|
158
|
+
continue
|
|
159
|
+
|
|
125
160
|
try:
|
|
126
|
-
self._log.
|
|
127
|
-
|
|
128
|
-
|
|
161
|
+
self._log.debug(
|
|
162
|
+
f"Module {module.get_address()} ({module.get_address():#02x}) detected: start loading"
|
|
163
|
+
)
|
|
129
164
|
await asyncio.wait_for(
|
|
130
|
-
|
|
131
|
-
|
|
165
|
+
module.load(from_cache=True),
|
|
166
|
+
SCAN_MODULEINFO_TIMEOUT_INITIAL / 1000.0,
|
|
132
167
|
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
self._scan_delay_msec = module.get_initial_timeout()
|
|
169
|
+
while self._scan_delay_msec > 50 and not await module.is_loaded():
|
|
170
|
+
# self._log.debug(
|
|
171
|
+
# f"\t... waiting {self._scan_delay_msec} is_loaded={await module.is_loaded()}"
|
|
172
|
+
# )
|
|
173
|
+
self._scan_delay_msec = self._scan_delay_msec - 50
|
|
174
|
+
await asyncio.sleep(0.05)
|
|
175
|
+
module_scan_time = time.perf_counter() - start_module_scan
|
|
136
176
|
self._log.info(
|
|
137
|
-
f"Scan module {address}
|
|
177
|
+
f"Scan module {address} ({address:#02x}, {module.get_type_name()}) completed in {module_scan_time:.2f}, module loaded={await module.is_loaded()}"
|
|
138
178
|
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
self.
|
|
179
|
+
except asyncio.TimeoutError:
|
|
180
|
+
self._log.error(
|
|
181
|
+
f"Module {address} ({address:#02x}) did not respond to info requests after successful type request"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
self._scan_complete = True
|
|
185
|
+
total_time = time.perf_counter() - start_time
|
|
186
|
+
self._log.info(f"Module scan completed in {total_time:.2f} seconds")
|
|
187
|
+
|
|
188
|
+
async def __handle_module_type_response_async(self, rawmsg: RawMessage) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Handle a received module type response packet
|
|
191
|
+
"""
|
|
192
|
+
address = rawmsg.address
|
|
193
|
+
|
|
194
|
+
if self.__scan_found_addresses is None:
|
|
195
|
+
self._log.warning(
|
|
196
|
+
f"Received module type response for address {address} ({address:#02x}) but no scan in progress"
|
|
197
|
+
)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
tmsg: ModuleTypeMessage = ModuleTypeMessage()
|
|
201
|
+
tmsg.populate(rawmsg.priority, address, rawmsg.rtr, rawmsg.data_only)
|
|
202
|
+
self._log.debug(
|
|
203
|
+
f"A '{tmsg.module_type_name()}' ({tmsg.module_type:#02x}) lives on address {address} ({address:#02x})"
|
|
204
|
+
)
|
|
205
|
+
self.__scan_found_addresses[address] = tmsg
|
|
166
206
|
|
|
167
207
|
async def handle(self, rawmsg: RawMessage) -> None:
|
|
168
208
|
"""
|
|
@@ -180,20 +220,8 @@ class PacketHandler:
|
|
|
180
220
|
data = rawmsg.data_only
|
|
181
221
|
|
|
182
222
|
# handle module type response message
|
|
183
|
-
if command_value == 0xFF
|
|
184
|
-
|
|
185
|
-
tmsg.populate(priority, address, rtr, data)
|
|
186
|
-
async with self._scanLock:
|
|
187
|
-
await self._handle_module_type(tmsg)
|
|
188
|
-
if address == self._modulescan_address:
|
|
189
|
-
self._typeResponseReceived.set()
|
|
190
|
-
else:
|
|
191
|
-
self._log.debug(
|
|
192
|
-
f"Unexpected module type message module address {address}, Velbuslink scan?"
|
|
193
|
-
)
|
|
194
|
-
self._modulescan_address = address - 1
|
|
195
|
-
|
|
196
|
-
self._typeResponseReceived.set()
|
|
223
|
+
if command_value == 0xFF:
|
|
224
|
+
await self.__handle_module_type_response_async(rawmsg)
|
|
197
225
|
|
|
198
226
|
# handle module subtype response message
|
|
199
227
|
elif command_value in (0xB0, 0xA7, 0xA6) and not self._scan_complete:
|
velbusaio/message.py
CHANGED
|
@@ -4,6 +4,7 @@ The velbus abstract message class
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import enum
|
|
7
8
|
import json
|
|
8
9
|
|
|
9
10
|
from velbusaio.const import PRIORITY_FIRMWARE, PRIORITY_HIGH, PRIORITY_LOW
|
|
@@ -65,8 +66,13 @@ class Message:
|
|
|
65
66
|
continue
|
|
66
67
|
if callable(getattr(self, key)) or key.startswith("__"):
|
|
67
68
|
del me[key]
|
|
68
|
-
if isinstance(me[key], (bytes, bytearray)):
|
|
69
|
+
if isinstance(me[key], (bytes, bytearray, enum.Enum)):
|
|
69
70
|
me[key] = str(me[key])
|
|
71
|
+
else:
|
|
72
|
+
try:
|
|
73
|
+
json.dumps(me[key]) # Test if the value is JSON serializable
|
|
74
|
+
except (TypeError, ValueError):
|
|
75
|
+
me[key] = str(me[key]) # Convert non-serializable objects to string
|
|
70
76
|
return me
|
|
71
77
|
|
|
72
78
|
def to_json(self) -> str:
|
velbusaio/module.py
CHANGED
|
@@ -165,6 +165,7 @@ class Module:
|
|
|
165
165
|
self._is_loading = False
|
|
166
166
|
self._channels = {}
|
|
167
167
|
self.loaded = False
|
|
168
|
+
self._loaded_cache = {}
|
|
168
169
|
|
|
169
170
|
def get_initial_timeout(self) -> int:
|
|
170
171
|
return SCAN_MODULEINFO_TIMEOUT_INITIAL
|
|
@@ -576,12 +577,8 @@ class Module:
|
|
|
576
577
|
# start the loading
|
|
577
578
|
self._is_loading = True
|
|
578
579
|
# see if we have a cache
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
async with async_open(cfile, "r") as fl:
|
|
582
|
-
cache = json.loads(await fl.read())
|
|
583
|
-
except OSError:
|
|
584
|
-
cache = {}
|
|
580
|
+
cache = await self._get_cache()
|
|
581
|
+
self._loaded_cache = cache
|
|
585
582
|
# load default channels
|
|
586
583
|
await self._load_default_channels()
|
|
587
584
|
|
|
@@ -596,8 +593,8 @@ class Module:
|
|
|
596
593
|
if "channels" in cache:
|
|
597
594
|
for num, chan in cache["channels"].items():
|
|
598
595
|
self._channels[int(num)]._name = chan["name"]
|
|
599
|
-
if "
|
|
600
|
-
self._channels[int(num)]._sub_device = chan["
|
|
596
|
+
if "subdevice" in chan:
|
|
597
|
+
self._channels[int(num)]._sub_device = chan["subdevice"]
|
|
601
598
|
else:
|
|
602
599
|
self._channels[int(num)]._sub_device = False
|
|
603
600
|
if "Unit" in chan:
|
|
@@ -611,6 +608,15 @@ class Module:
|
|
|
611
608
|
self._is_loading = False
|
|
612
609
|
await self._request_module_status()
|
|
613
610
|
|
|
611
|
+
async def _get_cache(self):
|
|
612
|
+
try:
|
|
613
|
+
cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.json")
|
|
614
|
+
async with async_open(cfile, "r") as fl:
|
|
615
|
+
cache = json.loads(await fl.read())
|
|
616
|
+
except OSError:
|
|
617
|
+
cache = {}
|
|
618
|
+
return cache
|
|
619
|
+
|
|
614
620
|
def _load(self) -> None:
|
|
615
621
|
"""
|
|
616
622
|
Method for per module type loading
|
|
@@ -864,7 +870,25 @@ class VmbDali(Module):
|
|
|
864
870
|
if message.channel in self._channels:
|
|
865
871
|
del self._channels[message.channel]
|
|
866
872
|
elif message.data.device_type == DaliDeviceType.LedModule:
|
|
867
|
-
|
|
873
|
+
cache = self._loaded_cache
|
|
874
|
+
if (
|
|
875
|
+
"channels" in cache
|
|
876
|
+
and str(message.channel) in cache["channels"]
|
|
877
|
+
and cache["channels"][str(message.channel)]["type"] == "Dimmer"
|
|
878
|
+
):
|
|
879
|
+
# If we have a cached dimmer channel, use that name
|
|
880
|
+
name = cache["channels"][str(message.channel)]["name"]
|
|
881
|
+
self._channels[message.channel] = Dimmer(
|
|
882
|
+
self,
|
|
883
|
+
message.channel,
|
|
884
|
+
name,
|
|
885
|
+
False, # set False to enable an already loaded Dimmer
|
|
886
|
+
True,
|
|
887
|
+
self._writer,
|
|
888
|
+
self._address,
|
|
889
|
+
slider_scale=254,
|
|
890
|
+
)
|
|
891
|
+
elif self._channels.get(message.channel).__class__ != Dimmer:
|
|
868
892
|
# New or changed type, replace channel:
|
|
869
893
|
self._channels[message.channel] = Dimmer(
|
|
870
894
|
self,
|
|
@@ -921,8 +945,6 @@ class VmbDali(Module):
|
|
|
921
945
|
else:
|
|
922
946
|
return await super().on_message(message)
|
|
923
947
|
|
|
924
|
-
await self._cache()
|
|
925
|
-
|
|
926
948
|
async def _request_channel_name(self) -> None:
|
|
927
949
|
# Channel names are requested after channel scan
|
|
928
950
|
# don't do them here (at initialization time)
|
velbusaio/protocol.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import binascii
|
|
5
5
|
import logging
|
|
6
|
+
import time
|
|
6
7
|
import typing as t
|
|
7
8
|
from asyncio import transports
|
|
8
9
|
|
|
@@ -180,20 +181,22 @@ class VelbusProtocol(asyncio.BufferedProtocol):
|
|
|
180
181
|
await self._write_transport_lock.acquire()
|
|
181
182
|
while self._restart_writer:
|
|
182
183
|
# wait for an item from the queue
|
|
183
|
-
msg_info = await self._send_queue.get()
|
|
184
|
+
msg_info: RawMessage | None = await self._send_queue.get()
|
|
184
185
|
if msg_info is None:
|
|
185
186
|
self._restart_writer = False
|
|
186
187
|
return
|
|
187
188
|
message_sent = False
|
|
188
189
|
try:
|
|
190
|
+
start_time = time.perf_counter()
|
|
189
191
|
while not message_sent:
|
|
190
192
|
message_sent = await self._write_message(msg_info)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
send_time = time.perf_counter() - start_time
|
|
194
|
+
|
|
195
|
+
self._send_queue.task_done() # indicate that the item of the queue has been processed
|
|
196
|
+
|
|
197
|
+
queue_sleep_time = self._calculate_queue_sleep_time(msg_info, send_time)
|
|
196
198
|
await asyncio.sleep(queue_sleep_time)
|
|
199
|
+
|
|
197
200
|
except (asyncio.CancelledError, GeneratorExit) as exc:
|
|
198
201
|
if not self._closing:
|
|
199
202
|
self._log.error(f"Stopping Velbus writer due to {exc!r}")
|
|
@@ -205,6 +208,22 @@ class VelbusProtocol(asyncio.BufferedProtocol):
|
|
|
205
208
|
self._write_transport_lock.release()
|
|
206
209
|
self._log.debug("Ending Velbus write message from send queue")
|
|
207
210
|
|
|
211
|
+
@staticmethod
|
|
212
|
+
def _calculate_queue_sleep_time(msg_info, send_time):
|
|
213
|
+
sleep_time = SLEEP_TIME
|
|
214
|
+
|
|
215
|
+
if msg_info.rtr:
|
|
216
|
+
sleep_time = SLEEP_TIME # this is a scan command. We could be quicker?
|
|
217
|
+
|
|
218
|
+
if msg_info.command == 0xEF:
|
|
219
|
+
# 'channel name request' command provokes in worst case 99 answer packets from VMBGPOD
|
|
220
|
+
sleep_time = SLEEP_TIME * 33 # TODO make this adaptable on module_type
|
|
221
|
+
|
|
222
|
+
if send_time > sleep_time:
|
|
223
|
+
return 0 # no need to wait, we are already late
|
|
224
|
+
else:
|
|
225
|
+
return sleep_time - send_time
|
|
226
|
+
|
|
208
227
|
@backoff.on_predicate(
|
|
209
228
|
backoff.expo,
|
|
210
229
|
lambda is_sent: not is_sent,
|
|
@@ -218,3 +237,7 @@ class VelbusProtocol(asyncio.BufferedProtocol):
|
|
|
218
237
|
return True
|
|
219
238
|
else:
|
|
220
239
|
return False
|
|
240
|
+
|
|
241
|
+
async def wait_on_all_messages_sent_async(self) -> None:
|
|
242
|
+
self._log.debug("Waiting on all messages sent")
|
|
243
|
+
await self._send_queue.join()
|
|
File without changes
|
|
File without changes
|