velbus-aio 2021.8.7__py3-none-any.whl → 2025.11.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.
Files changed (200) hide show
  1. scripts/parse_specs.py +156 -0
  2. velbus_aio-2025.11.0.dist-info/METADATA +71 -0
  3. velbus_aio-2025.11.0.dist-info/RECORD +194 -0
  4. {velbus_aio-2021.8.7.dist-info → velbus_aio-2025.11.0.dist-info}/WHEEL +1 -1
  5. velbus_aio-2025.11.0.dist-info/top_level.txt +3 -0
  6. velbusaio/channels.py +443 -109
  7. velbusaio/command_registry.py +126 -13
  8. velbusaio/const.py +36 -12
  9. velbusaio/controller.py +252 -177
  10. velbusaio/discovery.py +2 -2
  11. velbusaio/exceptions.py +22 -0
  12. velbusaio/handler.py +311 -145
  13. velbusaio/helpers.py +6 -18
  14. velbusaio/message.py +46 -132
  15. velbusaio/messages/__init__.py +12 -2
  16. velbusaio/messages/blind_status.py +16 -25
  17. velbusaio/messages/bus_active.py +3 -9
  18. velbusaio/messages/bus_error_counter_status.py +3 -4
  19. velbusaio/messages/bus_error_counter_status_request.py +3 -4
  20. velbusaio/messages/bus_off.py +3 -4
  21. velbusaio/messages/channel_name_part1.py +49 -33
  22. velbusaio/messages/channel_name_part2.py +49 -33
  23. velbusaio/messages/channel_name_part3.py +49 -33
  24. velbusaio/messages/channel_name_request.py +26 -12
  25. velbusaio/messages/clear_led.py +3 -4
  26. velbusaio/messages/counter_status.py +3 -17
  27. velbusaio/messages/counter_status_request.py +6 -6
  28. velbusaio/messages/counter_value.py +44 -0
  29. velbusaio/messages/cover_down.py +4 -29
  30. velbusaio/messages/cover_off.py +5 -29
  31. velbusaio/messages/cover_position.py +4 -19
  32. velbusaio/messages/cover_up.py +4 -27
  33. velbusaio/messages/dali_device_settings.py +178 -0
  34. velbusaio/messages/dali_device_settings_request.py +53 -0
  35. velbusaio/messages/dali_dim_value_status.py +44 -0
  36. velbusaio/messages/dimmer_channel_status.py +6 -19
  37. velbusaio/messages/dimmer_status.py +14 -31
  38. velbusaio/messages/edge_set_color.py +114 -0
  39. velbusaio/messages/edge_set_custom_color.py +56 -0
  40. velbusaio/messages/fast_blinking_led.py +3 -4
  41. velbusaio/messages/forced_off.py +3 -4
  42. velbusaio/messages/forced_on.py +3 -4
  43. velbusaio/messages/interface_status_request.py +3 -4
  44. velbusaio/messages/ir_receiver_status.py +18 -0
  45. velbusaio/messages/kwh_status.py +3 -19
  46. velbusaio/messages/light_value_request.py +3 -4
  47. velbusaio/messages/memo_text.py +3 -5
  48. velbusaio/messages/memory_data.py +3 -16
  49. velbusaio/messages/memory_data_block.py +3 -4
  50. velbusaio/messages/memory_dump_request.py +3 -4
  51. velbusaio/messages/module_status.py +107 -55
  52. velbusaio/messages/module_status_request.py +7 -6
  53. velbusaio/messages/module_subtype.py +11 -19
  54. velbusaio/messages/module_type.py +132 -21
  55. velbusaio/messages/module_type_request.py +1 -0
  56. velbusaio/messages/psu_load.py +56 -0
  57. velbusaio/messages/psu_values.py +53 -0
  58. velbusaio/messages/push_button_status.py +3 -16
  59. velbusaio/messages/raw.py +74 -0
  60. velbusaio/messages/read_data_block_from_memory.py +3 -4
  61. velbusaio/messages/read_data_from_memory.py +3 -4
  62. velbusaio/messages/realtime_clock_status_request.py +3 -4
  63. velbusaio/messages/receive_buffer_full.py +3 -4
  64. velbusaio/messages/receive_ready.py +3 -4
  65. velbusaio/messages/relay_status.py +13 -42
  66. velbusaio/messages/restore_dimmer.py +33 -24
  67. velbusaio/messages/select_program.py +35 -0
  68. velbusaio/messages/sensor_settings_request.py +3 -4
  69. velbusaio/messages/sensor_temp_request.py +3 -4
  70. velbusaio/messages/sensor_temperature.py +15 -19
  71. velbusaio/messages/set_date.py +10 -30
  72. velbusaio/messages/set_daylight_saving.py +8 -24
  73. velbusaio/messages/set_dimmer.py +43 -41
  74. velbusaio/messages/set_led.py +3 -4
  75. velbusaio/messages/set_realtime_clock.py +10 -30
  76. velbusaio/messages/set_temperature.py +3 -4
  77. velbusaio/messages/slider_status.py +16 -20
  78. velbusaio/messages/slow_blinking_led.py +3 -4
  79. velbusaio/messages/start_relay_blinking_timer.py +3 -4
  80. velbusaio/messages/start_relay_timer.py +3 -4
  81. velbusaio/messages/switch_relay_off.py +3 -16
  82. velbusaio/messages/switch_relay_on.py +3 -16
  83. velbusaio/messages/switch_to_comfort.py +4 -15
  84. velbusaio/messages/switch_to_day.py +4 -15
  85. velbusaio/messages/switch_to_night.py +4 -15
  86. velbusaio/messages/switch_to_safe.py +4 -15
  87. velbusaio/messages/temp_sensor_settings_part1.py +3 -4
  88. velbusaio/messages/temp_sensor_settings_part2.py +27 -0
  89. velbusaio/messages/temp_sensor_settings_part3.py +27 -0
  90. velbusaio/messages/temp_sensor_settings_part4.py +27 -0
  91. velbusaio/messages/temp_sensor_settings_request.py +3 -4
  92. velbusaio/messages/temp_sensor_status.py +34 -35
  93. velbusaio/messages/temp_set_cooling.py +3 -13
  94. velbusaio/messages/temp_set_heating.py +3 -13
  95. velbusaio/messages/update_led_status.py +3 -4
  96. velbusaio/messages/very_fast_blinking_led.py +3 -4
  97. velbusaio/messages/write_data_to_memory.py +3 -4
  98. velbusaio/messages/write_memory_block.py +3 -4
  99. velbusaio/messages/write_module_address_and_serial_number.py +3 -4
  100. velbusaio/module.py +680 -158
  101. velbusaio/module_spec/01.json +62 -0
  102. velbusaio/module_spec/02.json +16 -0
  103. velbusaio/module_spec/03.json +23 -0
  104. velbusaio/module_spec/04.json +283 -0
  105. velbusaio/module_spec/05.json +54 -0
  106. velbusaio/module_spec/06.json +110 -0
  107. velbusaio/module_spec/07.json +16 -0
  108. velbusaio/module_spec/08.json +38 -0
  109. velbusaio/module_spec/09.json +30 -0
  110. velbusaio/module_spec/0A.json +58 -0
  111. velbusaio/module_spec/0B.json +58 -0
  112. velbusaio/module_spec/0C.json +18 -0
  113. velbusaio/module_spec/0E.json +25 -0
  114. velbusaio/module_spec/0F.json +16 -0
  115. velbusaio/module_spec/10.json +111 -0
  116. velbusaio/module_spec/11.json +111 -0
  117. velbusaio/module_spec/12.json +73 -0
  118. velbusaio/module_spec/13.json +4 -0
  119. velbusaio/module_spec/14.json +16 -0
  120. velbusaio/module_spec/15.json +83 -0
  121. velbusaio/module_spec/16.json +129 -0
  122. velbusaio/module_spec/17.json +129 -0
  123. velbusaio/module_spec/18.json +129 -0
  124. velbusaio/module_spec/1A.json +79 -0
  125. velbusaio/module_spec/1B.json +107 -0
  126. velbusaio/module_spec/1D.json +89 -0
  127. velbusaio/module_spec/1E.json +306 -0
  128. velbusaio/module_spec/1F.json +178 -0
  129. velbusaio/module_spec/20.json +178 -0
  130. velbusaio/module_spec/21.json +326 -0
  131. velbusaio/module_spec/22.json +426 -0
  132. velbusaio/module_spec/23.json +129 -0
  133. velbusaio/module_spec/24.json +30 -0
  134. velbusaio/module_spec/25.json +3 -0
  135. velbusaio/module_spec/28.json +454 -0
  136. velbusaio/module_spec/29.json +235 -0
  137. velbusaio/module_spec/2A.json +239 -0
  138. velbusaio/module_spec/2B.json +239 -0
  139. velbusaio/module_spec/2C.json +257 -0
  140. velbusaio/module_spec/2D.json +270 -0
  141. velbusaio/module_spec/2E.json +215 -0
  142. velbusaio/module_spec/2F.json +211 -0
  143. velbusaio/module_spec/30.json +58 -0
  144. velbusaio/module_spec/31.json +465 -0
  145. velbusaio/module_spec/32.json +385 -0
  146. velbusaio/module_spec/33.json +249 -0
  147. velbusaio/module_spec/34.json +313 -0
  148. velbusaio/module_spec/35.json +313 -0
  149. velbusaio/module_spec/36.json +313 -0
  150. velbusaio/module_spec/37.json +333 -0
  151. velbusaio/module_spec/38.json +111 -0
  152. velbusaio/module_spec/39.json +4 -0
  153. velbusaio/module_spec/3A.json +306 -0
  154. velbusaio/module_spec/3B.json +306 -0
  155. velbusaio/module_spec/3C.json +306 -0
  156. velbusaio/module_spec/3D.json +454 -0
  157. velbusaio/module_spec/3E.json +302 -0
  158. velbusaio/module_spec/3F.json +4 -0
  159. velbusaio/module_spec/40.json +4 -0
  160. velbusaio/module_spec/41.json +241 -0
  161. velbusaio/module_spec/42.json +4 -0
  162. velbusaio/module_spec/43.json +23 -0
  163. velbusaio/module_spec/44.json +38 -0
  164. velbusaio/module_spec/45.json +4 -0
  165. velbusaio/module_spec/48.json +111 -0
  166. velbusaio/module_spec/49.json +111 -0
  167. velbusaio/module_spec/4A.json +89 -0
  168. velbusaio/module_spec/4B.json +138 -0
  169. velbusaio/module_spec/4C.json +129 -0
  170. velbusaio/module_spec/4D.json +108 -0
  171. velbusaio/module_spec/4E.json +787 -0
  172. velbusaio/module_spec/4F.json +114 -0
  173. velbusaio/module_spec/50.json +114 -0
  174. velbusaio/module_spec/51.json +114 -0
  175. velbusaio/module_spec/52.json +456 -0
  176. velbusaio/module_spec/54.json +270 -0
  177. velbusaio/module_spec/55.json +270 -0
  178. velbusaio/module_spec/56.json +270 -0
  179. velbusaio/module_spec/57.json +260 -0
  180. velbusaio/module_spec/5A.json +4 -0
  181. velbusaio/module_spec/5B.json +4 -0
  182. velbusaio/module_spec/5C.json +90 -0
  183. velbusaio/module_spec/5F.json +78 -0
  184. velbusaio/module_spec/60.json +4 -0
  185. velbusaio/module_spec/61.json +89 -0
  186. velbusaio/module_spec/broadcast.json +67 -0
  187. velbusaio/module_spec/ignore.json +22 -0
  188. velbusaio/protocol.py +243 -0
  189. velbusaio/py.typed +0 -0
  190. velbusaio/raw_message.py +149 -0
  191. velbusaio/util.py +55 -0
  192. velbusaio/vlp_reader.py +249 -0
  193. velbus_aio-2021.8.7.dist-info/METADATA +0 -66
  194. velbus_aio-2021.8.7.dist-info/RECORD +0 -90
  195. velbus_aio-2021.8.7.dist-info/top_level.txt +0 -1
  196. velbusaio/messages/meteo_raw.py +0 -52
  197. velbusaio/module_registry.py +0 -64
  198. velbusaio/moduleprotocol/protocol.json +0 -25540
  199. velbusaio/parser.py +0 -142
  200. {velbus_aio-2021.8.7.dist-info → velbus_aio-2025.11.0.dist-info/licenses}/LICENSE +0 -0
velbusaio/handler.py CHANGED
@@ -2,184 +2,350 @@
2
2
  Velbus packet handler
3
3
  :Author maikel punie <maikel.punie@gmail.com>
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
8
+ import asyncio
9
+ import importlib.resources
7
10
  import json
8
11
  import logging
9
- import re
12
+ import os
13
+ import pathlib
14
+ import sys
15
+ import time
16
+ from typing import TYPE_CHECKING
10
17
 
11
- import pkg_resources
18
+ from aiofile import async_open
12
19
 
13
20
  from velbusaio.command_registry import commandRegistry
14
- from velbusaio.const import RTR
15
- from velbusaio.helpers import h2, keys_exists
16
- from velbusaio.message import Message
21
+ from velbusaio.const import (
22
+ SCAN_MODULEINFO_TIMEOUT_INITIAL,
23
+ SCAN_MODULEINFO_TIMEOUT_INTERVAL,
24
+ SCAN_MODULETYPE_TIMEOUT,
25
+ )
17
26
  from velbusaio.messages.module_subtype import ModuleSubTypeMessage
18
- from velbusaio.messages.module_type import ModuleTypeMessage
27
+ from velbusaio.messages.module_type import ModuleType2Message, ModuleTypeMessage
28
+ from velbusaio.module import Module
29
+ from velbusaio.raw_message import RawMessage
30
+
31
+ if TYPE_CHECKING:
32
+ from velbusaio.controller import Velbus
19
33
 
20
34
 
21
35
  class PacketHandler:
22
36
  """
23
- The packetHandler class
37
+ The PacketHandler class
24
38
  """
25
39
 
26
- def __init__(self, writer, velbus) -> None:
27
- self._log = logging.getLogger("velbus-packet")
40
+ def __init__(
41
+ self,
42
+ velbus: Velbus,
43
+ one_address: int | None = None,
44
+ ) -> None:
45
+ self._log = logging.getLogger("velbus-handler")
28
46
  self._log.setLevel(logging.DEBUG)
29
- self._writer = writer
30
47
  self._velbus = velbus
48
+ self._one_address = one_address
49
+ self._typeResponseReceived = asyncio.Event()
50
+ self._scanLock = asyncio.Lock()
51
+ self._fullScanLock = asyncio.Lock()
52
+ self._modulescan_address = 0
31
53
  self._scan_complete = False
32
- with open(
33
- pkg_resources.resource_filename(__name__, "moduleprotocol/protocol.json")
34
- ) as protocol_file:
35
- self.pdata = json.load(protocol_file)
54
+ self._scan_delay_msec = 0
55
+ self.__scan_found_addresses: dict[int, ModuleTypeMessage | None] | None = None
36
56
 
37
- def scan_finished(self) -> None:
38
- self._scan_complete = True
57
+ async def read_protocol_data(self):
58
+ if sys.version_info >= (3, 13):
59
+ with importlib.resources.path(
60
+ __name__, "module_spec/broadcast.json"
61
+ ) as fspath:
62
+ async with async_open(fspath) as protocol_file:
63
+ self.broadcast = json.loads(await protocol_file.read())
64
+ with importlib.resources.path(
65
+ __name__, "module_spec/ignore.json"
66
+ ) as fspath:
67
+ async with async_open(fspath) as protocol_file:
68
+ self.ignore = json.loads(await protocol_file.read())
69
+ else:
70
+ async with async_open(
71
+ str(
72
+ importlib.resources.files(__name__.split(".")[0]).joinpath(
73
+ "module_spec/broadcast.json"
74
+ )
75
+ )
76
+ ) as protocol_file:
77
+ self.broadcast = json.loads(await protocol_file.read())
78
+ async with async_open(
79
+ str(
80
+ importlib.resources.files(__name__.split(".")[0]).joinpath(
81
+ "module_spec/ignore.json"
82
+ )
83
+ )
84
+ ) as protocol_file:
85
+ self.ignore = json.loads(await protocol_file.read())
39
86
 
40
- def scan_started(self) -> None:
41
- self._scan_complete = False
87
+ def empty_cache(self) -> bool:
88
+ if (
89
+ len(
90
+ [
91
+ name
92
+ for name in os.listdir(f"{self._velbus.get_cache_dir()}")
93
+ if os.path.isfile(f"{self._velbus.get_cache_dir()}/{name}")
94
+ ]
95
+ )
96
+ == 0
97
+ ):
98
+ return True
99
+ return False
100
+
101
+ async def scan(self, reload_cache: bool = False) -> None:
102
+ start_address = 1
103
+ max_address = 254 + 1
104
+ if self._one_address is not None:
105
+ start_address = self._one_address
106
+ max_address = self._one_address + 1
107
+ self._log.info(
108
+ f"Scanning only one address {self._one_address} ({self._one_address:#02x})"
109
+ )
110
+
111
+ self._log.info("Start module scan")
112
+ async with self._fullScanLock:
113
+ start_time = time.perf_counter()
114
+ self._scan_complete = False
115
+
116
+ self._log.debug("Waiting for Velbus bus to be ready to scan...")
117
+ await self._velbus.wait_on_all_messages_sent_async() # don't start a scan while messages are still in the queue
118
+ self._log.debug("Velbus bus is ready to scan!")
119
+
120
+ self._log.info("Sending scan type requests to all addresses...")
121
+ start_scan_time = time.perf_counter()
122
+ self.__scan_found_addresses = {}
123
+ for address in range(start_address, max_address):
124
+ cfile = pathlib.Path(f"{self._velbus.get_cache_dir()}/{address}.json")
125
+ if reload_cache and os.path.isfile(cfile):
126
+ self._log.info(
127
+ f"Reloading cache for address {address} ({address:#02x})"
128
+ )
129
+ os.remove(cfile)
130
+
131
+ self.__scan_found_addresses[address] = None
132
+ async with self._scanLock:
133
+ await self._velbus.sendTypeRequestMessage(address)
134
+
135
+ await self._velbus.wait_on_all_messages_sent_async()
136
+ scan_time = time.perf_counter() - start_scan_time
137
+ self._log.info(
138
+ f"Sent scan type requests to all addresses in {scan_time:.2f}. Going to wait for responses..."
139
+ )
140
+
141
+ await asyncio.sleep(SCAN_MODULETYPE_TIMEOUT / 1000) # wait for responses
142
+
143
+ self._log.info(
144
+ "Waiting for responses done. Going to check for responses..."
145
+ )
146
+ for address in range(start_address, max_address):
147
+ start_module_scan = time.perf_counter()
148
+ module_type_message: ModuleTypeMessage | None = (
149
+ self.__scan_found_addresses[address]
150
+ )
151
+ module: Module | None = None
152
+ if module_type_message is None:
153
+ self._log.debug(
154
+ f"No module found at address {address} ({address:#02x}). Skipping it."
155
+ )
156
+ continue
157
+
158
+ self._log.info(
159
+ f"Found module at address {address} ({address:#02x}): {module_type_message.module_type_name()}"
160
+ )
161
+ # cache_file = pathlib.Path(f"{self._velbus.get_cache_dir()}/{address}.json")
162
+ # TODO: check if cached file module type is the same?
163
+ await self._handle_module_type(module_type_message)
164
+ async with self._scanLock:
165
+ module = self._velbus.get_module(address)
166
+
167
+ if module is None:
168
+ self._log.info(
169
+ f"Module at address {address} ({address:#02x}) could not be loaded. Skipping it."
170
+ )
171
+ continue
172
+
173
+ try:
174
+ self._log.debug(
175
+ f"Module {module.get_address()} ({module.get_address():#02x}) detected: start loading"
176
+ )
177
+ await asyncio.wait_for(
178
+ module.load(from_cache=True),
179
+ SCAN_MODULEINFO_TIMEOUT_INITIAL / 1000.0,
180
+ )
181
+ self._scan_delay_msec = module.get_initial_timeout()
182
+ while self._scan_delay_msec > 50 and not await module.is_loaded():
183
+ # self._log.debug(
184
+ # f"\t... waiting {self._scan_delay_msec} is_loaded={await module.is_loaded()}"
185
+ # )
186
+ self._scan_delay_msec = self._scan_delay_msec - 50
187
+ await asyncio.sleep(0.05)
188
+ module_scan_time = time.perf_counter() - start_module_scan
189
+ self._log.info(
190
+ f"Scan module {address} ({address:#02x}, {module.get_type_name()}) completed in {module_scan_time:.2f}, module loaded={await module.is_loaded()}"
191
+ )
192
+ await module.wait_for_status_messages()
193
+ except asyncio.TimeoutError:
194
+ self._log.error(
195
+ f"Module {address} ({address:#02x}) did not respond to info requests after successful type request"
196
+ )
197
+
198
+ self._scan_complete = True
199
+ total_time = time.perf_counter() - start_time
200
+ self._log.info(f"Module scan completed in {total_time:.2f} seconds")
42
201
 
43
- async def handle(self, data: str) -> None:
202
+ async def __handle_module_type_response_async(self, rawmsg: RawMessage) -> None:
44
203
  """
45
- Handle a recievd packet
204
+ Handle a received module type response packet
46
205
  """
47
- priority = data[1]
48
- address = int(data[2])
49
- rtr = data[3] & RTR == RTR
50
- data_size = data[3] & 0x0F
51
- command_value = data[4]
52
- if data_size < 1:
206
+ address = rawmsg.address
207
+
208
+ if self.__scan_found_addresses is None:
209
+ self._log.warning(
210
+ f"Received module type response for address {address} ({address:#02x}) but no scan in progress"
211
+ )
212
+ return
213
+
214
+ tmsg: ModuleTypeMessage = ModuleTypeMessage()
215
+ tmsg.populate(rawmsg.priority, address, rawmsg.rtr, rawmsg.data_only)
216
+ self._log.debug(
217
+ f"A '{tmsg.module_type_name()}' ({tmsg.module_type:#02x}) lives on address {address} ({address:#02x})"
218
+ )
219
+ self.__scan_found_addresses[address] = tmsg
220
+
221
+ async def handle(self, rawmsg: RawMessage) -> None:
222
+ """
223
+ Handle a received packet
224
+ """
225
+ if rawmsg.address < 1 or rawmsg.address > 254:
53
226
  return
54
- if address < 1 or address > 254:
227
+ if rawmsg.command is None:
55
228
  return
56
- if command_value == 0xFF and not self._scan_complete:
57
- msg = ModuleTypeMessage()
58
- msg.populate(priority, address, rtr, data[5:-2])
59
- self._log.debug(f"Received {msg}")
60
- await self._handle_module_type(msg)
61
- elif command_value == 0xB0 and not self._scan_complete:
62
- msg = ModuleSubTypeMessage()
63
- msg.populate(priority, address, rtr, data[5:-2])
64
- self._log.debug(f"Received {msg}")
65
- await self._handle_module_subtype(msg)
66
- elif command_value in self.pdata["MessagesBroadCast"]:
229
+
230
+ priority = rawmsg.priority
231
+ address = rawmsg.address
232
+ rtr = rawmsg.rtr
233
+ command_value = rawmsg.command
234
+ data = rawmsg.data_only
235
+
236
+ # handle module type response message
237
+ if command_value == 0xFF:
238
+ await self.__handle_module_type_response_async(rawmsg)
239
+
240
+ # handle module subtype response message
241
+ elif command_value in (0xB0, 0xA7, 0xA6) and not self._scan_complete:
242
+ msg: ModuleSubTypeMessage = ModuleSubTypeMessage()
243
+ msg.populate(priority, address, rtr, data)
244
+ if command_value == 0xB0:
245
+ msg.sub_address_offset = 0
246
+ elif command_value == 0xA7:
247
+ msg.sub_address_offset = 4
248
+ elif command_value == 0xA6:
249
+ msg.sub_address_offset = 8
250
+ async with self._scanLock:
251
+ self._scan_delay_msec += SCAN_MODULEINFO_TIMEOUT_INTERVAL
252
+ self._handle_module_subtype(msg)
253
+
254
+ # ignore broadcast
255
+ elif command_value in self.broadcast:
67
256
  self._log.debug(
68
257
  "Received broadcast message {} from {}, ignoring".format(
69
- self.pdata["MessageBroadCast"][command_value.upper()], address
258
+ self.broadcast[str(command_value).upper()], address
70
259
  )
71
260
  )
72
- elif address in self._velbus.get_modules().keys():
73
- module_type = self._velbus.get_module(address).get_type()
74
- if commandRegistry.has_command(int(command_value), module_type):
75
- command = commandRegistry.get_command(command_value, module_type)
76
- msg = command()
77
- msg.populate(priority, address, rtr, data[5:-2])
78
- self._log.debug(f"Received {msg}")
79
- # send the message to the modules
80
- await (self._velbus.get_module(msg.address)).on_message(msg)
81
- else:
82
- self._log.warning(
83
- "NOT FOUND IN command_registry: addr={} cmd={} packet={}".format(
84
- address, command_value, ":".join(format(x, "02x") for x in data)
85
- )
86
- )
87
- elif self._scan_complete:
88
- # this should only happen once the scan is complete, of its not complete susppend the error message
89
- self._log.warning(
90
- "UNKNOWN module, you should iniitalize a full new velbus scan: packet={}, address={}, modules={}".format(
91
- ":".join(format(x, "02x") for x in data),
92
- address,
93
- self._velbus.get_modules().keys(),
261
+
262
+ # ignore messages
263
+ elif command_value in self.ignore:
264
+ self._log.debug(
265
+ "Received ignored message {} from {}, ignoring".format(
266
+ self.ignore[str(command_value).upper()], address
94
267
  )
95
268
  )
96
269
 
97
- def _per_byte(self, cmsg, msg) -> dict:
98
- result = {}
99
- for num, byte in enumerate(msg.data):
100
- num = str(num)
101
- # only do something if its defined
102
- if num not in cmsg:
103
- continue
104
- # check if we can do a binary match
105
- for mat in cmsg[num]["Match"]:
106
- if (
107
- (mat.startswith("%") and re.match(mat[1:], f"{byte:08b}"))
108
- or mat == f"{byte:08b}"
109
- or mat == f"{byte:02x}"
110
- ):
111
- result = self._per_byte_handle(
112
- result, cmsg[num]["Match"][mat], byte
113
- )
114
- return result
115
-
116
- def _per_byte_handle(self, result: dict, todo: dict, byte: int) -> dict:
117
- if "Channel" in todo:
118
- result["Channel"] = todo["Channel"]
119
- if "Value" in todo:
120
- result["Value"] = todo["Value"]
121
- if "Convert" in todo:
122
- result["ValueList"] = []
123
- if todo["Convert"] == "Decimal":
124
- result["ValueList"].append(int(byte))
125
- elif todo["Convert"] == "Counter":
126
- result["ValueList"].append(f"{byte:02x}")
127
- elif todo["Convert"] == "Temperature":
128
- print("CONVERT temperature")
129
- elif todo["Convert"] == "Divider":
130
- bin_str = f"{byte:08b}"
131
- chan = bin_str[6:]
132
- val = bin_str[:5]
133
- print(f"CONVERT Divider {chan} {val}")
134
- elif todo["Convert"] == "Channel":
135
- print("CONVERT Channel")
136
- elif todo["Convert"] == "ChannelBit":
137
- print("CONVERT ChannelBit")
138
- elif todo["Convert"].startswith("ChannelBitStatus"):
139
- print("CONVERT ChannelBitStatus")
140
- else:
141
- self._log.error("UNKNOWN convert requested: {}".format(todo["Convert"]))
142
- return result
270
+ # handle other messages for modules that are already scanned
271
+ else:
272
+ module = None
273
+ async with self._scanLock:
274
+ module = self._velbus.get_module(address)
275
+ if module is not None:
276
+ module_type = module.get_type()
277
+ if commandRegistry.has_command(int(command_value), module_type):
278
+ command = commandRegistry.get_command(command_value, module_type)
279
+ if not command:
280
+ return
281
+ msg = command()
282
+ msg.populate(priority, address, rtr, data)
283
+ # restart the info completion time when info message received
284
+ if command_value in (
285
+ 0xF0,
286
+ 0xF1,
287
+ 0xF2,
288
+ 0xFB,
289
+ 0xFE,
290
+ 0xCC,
291
+ ): # names, memory data, memory block
292
+ self._scan_delay_msec += SCAN_MODULEINFO_TIMEOUT_INTERVAL
293
+ # self._log.debug(f"Restart timeout {msg}")
294
+ # send the message to the modules
295
+ await module.on_message(msg)
296
+ else:
297
+ self._log.warning(f"NOT FOUND IN command_registry: {rawmsg}")
143
298
 
144
- async def _handle_module_type(self, msg: Message) -> None:
299
+ async def _handle_module_type(
300
+ self, msg: ModuleTypeMessage | ModuleType2Message
301
+ ) -> None:
145
302
  """
146
303
  load the module data
147
304
  """
148
- data = keys_exists(self.pdata, "ModuleTypes", h2(msg.module_type))
149
- if not data:
150
- self._log.warning(f"Module not recognized: {msg.module_type}")
151
- return
152
- # create the module
153
- await self._velbus.add_module(
154
- msg.address,
155
- msg.module_type,
156
- data,
157
- memorymap=msg.memory_map_version,
158
- build_year=msg.build_year,
159
- build_week=msg.build_week,
160
- serial=msg.serial,
161
- )
305
+ if msg is not None:
306
+ module = self._velbus.get_module(msg.address)
307
+ if module is None:
308
+ # data = keys_exists(self.pdata, "ModuleTypes", h2(msg.module_type))
309
+ # if not data:
310
+ # self._log.warning(f"Module not recognized: {msg.module_type}")
311
+ # return
312
+ await self._velbus.add_module(
313
+ msg.address,
314
+ msg.module_type,
315
+ memorymap=msg.memory_map_version,
316
+ build_year=msg.build_year,
317
+ build_week=msg.build_week,
318
+ serial=msg.serial,
319
+ )
320
+ else:
321
+ self._log.debug(
322
+ f"***Module already exists scanAddr={self._modulescan_address} addr={msg.address} {msg}"
323
+ )
162
324
 
163
- async def _handle_module_subtype(self, msg: Message) -> None:
164
- if msg.address not in self._velbus.get_modules():
165
- return
166
- addrList = {
167
- 1: msg.sub_address_1,
168
- 2: msg.sub_address_2,
169
- 3: msg.sub_address_3,
170
- 4: msg.sub_address_4,
171
- }
172
- await self._velbus.add_submodules(msg.address, addrList)
173
-
174
- def _channel_convert(self, module: str, channel: str, ctype: str) -> None | int:
175
- data = keys_exists(
176
- self.pdata, "ModuleTypes", h2(module), "ChannelNumbers", ctype
177
- )
178
- if data and "Map" in data and h2(channel) in data["Map"]:
179
- return data["Map"][h2(channel)]
180
- if data and "Convert" in data:
181
- return int(channel)
182
- for offset in range(0, 8):
183
- if channel & (1 << offset):
184
- return offset + 1
185
- return None
325
+ # else:
326
+ # self._log.debug("*** handle_module_type called without response message")
327
+
328
+ def _handle_module_subtype(self, msg: ModuleSubTypeMessage) -> None:
329
+ module = self._velbus.get_module(msg.address)
330
+ if module is not None:
331
+ addrList = {
332
+ (msg.sub_address_offset + 1): msg.sub_address_1,
333
+ (msg.sub_address_offset + 2): msg.sub_address_2,
334
+ (msg.sub_address_offset + 3): msg.sub_address_3,
335
+ (msg.sub_address_offset + 4): msg.sub_address_4,
336
+ }
337
+ self._velbus.add_submodules(module, addrList)
338
+
339
+
340
+ # def _channel_convert(self, module: str, channel: str, ctype: str) -> None | int:
341
+ # data = keys_exists(
342
+ # self.pdata, "ModuleTypes", h2(module), "ChannelNumbers", ctype
343
+ # )
344
+ # if data and "Map" in data and h2(channel) in data["Map"]:
345
+ # return data["Map"][h2(channel)]
346
+ # if data and "Convert" in data:
347
+ # return int(channel)
348
+ # for offset in range(0, 8):
349
+ # if channel & (1 << offset):
350
+ # return offset + 1
351
+ # return None
velbusaio/helpers.py CHANGED
@@ -1,15 +1,17 @@
1
1
  """
2
2
  Helper functions
3
3
  """
4
+
4
5
  from __future__ import annotations
5
6
 
6
7
  import os
7
8
  import re
9
+ from typing import Any
8
10
 
9
11
  from velbusaio.const import CACHEDIR
10
12
 
11
13
 
12
- def keys_exists(element, *keys) -> dict:
14
+ def keys_exists(element: dict[str, Any], *keys) -> dict:
13
15
  """
14
16
  Check if *keys (nested) exists in `element` (dict).
15
17
  """
@@ -27,28 +29,14 @@ def keys_exists(element, *keys) -> dict:
27
29
  return _element
28
30
 
29
31
 
30
- def checksum(arr) -> int:
31
- """
32
- Calculate checksum of the given array.
33
- The checksum is calculated by summing all values in an array, then performing the two's complement.
34
- :param arr: The array of bytes of which the checksum has to be calculated of.
35
- :return: The checksum of the given array.
36
- """
37
- crc = sum(arr)
38
- crc = crc ^ 255
39
- crc = crc + 1
40
- crc = crc & 255
41
- return crc
42
-
43
-
44
- def h2(inp) -> str:
32
+ def h2(inp: int) -> str:
45
33
  """
46
- Format as hex upercase
34
+ Format as hex uppercase
47
35
  """
48
36
  return format(inp, "02x").upper()
49
37
 
50
38
 
51
- def handle_match(match_dict, data) -> dict:
39
+ def handle_match(match_dict: dict[str, dict[str, dict[str, str]]], data: int) -> dict:
52
40
  """
53
41
  Handle memory match from the module data
54
42
  """