velbus-aio 2023.12.0__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 (192) hide show
  1. scripts/parse_specs.py +156 -0
  2. {velbus_aio-2023.12.0.dist-info → velbus_aio-2025.11.0.dist-info}/METADATA +24 -8
  3. velbus_aio-2025.11.0.dist-info/RECORD +194 -0
  4. {velbus_aio-2023.12.0.dist-info → velbus_aio-2025.11.0.dist-info}/WHEEL +1 -1
  5. {velbus_aio-2023.12.0.dist-info → velbus_aio-2025.11.0.dist-info}/top_level.txt +1 -0
  6. velbusaio/channels.py +35 -14
  7. velbusaio/command_registry.py +32 -5
  8. velbusaio/const.py +10 -2
  9. velbusaio/controller.py +183 -145
  10. velbusaio/handler.py +286 -154
  11. velbusaio/helpers.py +2 -1
  12. velbusaio/message.py +14 -8
  13. velbusaio/messages/__init__.py +6 -2
  14. velbusaio/messages/blind_status.py +4 -2
  15. velbusaio/messages/bus_active.py +1 -0
  16. velbusaio/messages/bus_error_counter_status.py +1 -0
  17. velbusaio/messages/bus_error_counter_status_request.py +1 -0
  18. velbusaio/messages/bus_off.py +1 -0
  19. velbusaio/messages/channel_name_part1.py +18 -0
  20. velbusaio/messages/channel_name_part2.py +18 -0
  21. velbusaio/messages/channel_name_part3.py +18 -0
  22. velbusaio/messages/channel_name_request.py +2 -1
  23. velbusaio/messages/clear_led.py +1 -0
  24. velbusaio/messages/counter_status.py +1 -0
  25. velbusaio/messages/counter_status_request.py +1 -0
  26. velbusaio/messages/counter_value.py +44 -0
  27. velbusaio/messages/cover_down.py +2 -1
  28. velbusaio/messages/cover_off.py +2 -3
  29. velbusaio/messages/cover_position.py +3 -4
  30. velbusaio/messages/cover_up.py +2 -1
  31. velbusaio/messages/dali_device_settings.py +2 -1
  32. velbusaio/messages/dali_device_settings_request.py +2 -1
  33. velbusaio/messages/dali_dim_value_status.py +4 -1
  34. velbusaio/messages/dimmer_channel_status.py +5 -1
  35. velbusaio/messages/dimmer_status.py +13 -1
  36. velbusaio/messages/edge_set_color.py +1 -0
  37. velbusaio/messages/edge_set_custom_color.py +1 -0
  38. velbusaio/messages/fast_blinking_led.py +1 -0
  39. velbusaio/messages/forced_off.py +1 -0
  40. velbusaio/messages/forced_on.py +1 -0
  41. velbusaio/messages/interface_status_request.py +1 -0
  42. velbusaio/messages/ir_receiver_status.py +1 -0
  43. velbusaio/messages/kwh_status.py +1 -0
  44. velbusaio/messages/light_value_request.py +1 -0
  45. velbusaio/messages/memo_text.py +1 -0
  46. velbusaio/messages/memory_data.py +1 -0
  47. velbusaio/messages/memory_data_block.py +1 -0
  48. velbusaio/messages/memory_dump_request.py +1 -0
  49. velbusaio/messages/module_status.py +13 -0
  50. velbusaio/messages/module_status_request.py +5 -2
  51. velbusaio/messages/module_subtype.py +4 -3
  52. velbusaio/messages/module_type.py +17 -7
  53. velbusaio/messages/module_type_request.py +1 -0
  54. velbusaio/messages/psu_load.py +56 -0
  55. velbusaio/messages/psu_values.py +53 -0
  56. velbusaio/messages/push_button_status.py +1 -0
  57. velbusaio/messages/raw.py +1 -0
  58. velbusaio/messages/read_data_block_from_memory.py +1 -0
  59. velbusaio/messages/read_data_from_memory.py +1 -0
  60. velbusaio/messages/realtime_clock_status_request.py +1 -0
  61. velbusaio/messages/receive_buffer_full.py +1 -0
  62. velbusaio/messages/receive_ready.py +1 -0
  63. velbusaio/messages/relay_status.py +1 -0
  64. velbusaio/messages/restore_dimmer.py +16 -2
  65. velbusaio/messages/select_program.py +1 -0
  66. velbusaio/messages/sensor_settings_request.py +1 -0
  67. velbusaio/messages/sensor_temp_request.py +1 -0
  68. velbusaio/messages/sensor_temperature.py +1 -0
  69. velbusaio/messages/set_date.py +5 -10
  70. velbusaio/messages/set_daylight_saving.py +3 -6
  71. velbusaio/messages/set_dimmer.py +22 -13
  72. velbusaio/messages/set_led.py +1 -0
  73. velbusaio/messages/set_realtime_clock.py +5 -10
  74. velbusaio/messages/set_temperature.py +1 -0
  75. velbusaio/messages/slider_status.py +15 -1
  76. velbusaio/messages/slow_blinking_led.py +1 -0
  77. velbusaio/messages/start_relay_blinking_timer.py +1 -0
  78. velbusaio/messages/start_relay_timer.py +1 -0
  79. velbusaio/messages/switch_relay_off.py +1 -0
  80. velbusaio/messages/switch_relay_on.py +1 -0
  81. velbusaio/messages/switch_to_comfort.py +1 -0
  82. velbusaio/messages/switch_to_day.py +1 -0
  83. velbusaio/messages/switch_to_night.py +1 -0
  84. velbusaio/messages/switch_to_safe.py +1 -0
  85. velbusaio/messages/temp_sensor_settings_part1.py +1 -0
  86. velbusaio/messages/temp_sensor_settings_part2.py +1 -0
  87. velbusaio/messages/temp_sensor_settings_part3.py +1 -0
  88. velbusaio/messages/temp_sensor_settings_part4.py +1 -0
  89. velbusaio/messages/temp_sensor_settings_request.py +1 -0
  90. velbusaio/messages/temp_sensor_status.py +1 -0
  91. velbusaio/messages/temp_set_cooling.py +1 -0
  92. velbusaio/messages/temp_set_heating.py +1 -0
  93. velbusaio/messages/update_led_status.py +1 -0
  94. velbusaio/messages/very_fast_blinking_led.py +1 -0
  95. velbusaio/messages/write_data_to_memory.py +1 -0
  96. velbusaio/messages/write_memory_block.py +1 -0
  97. velbusaio/messages/write_module_address_and_serial_number.py +1 -0
  98. velbusaio/module.py +214 -102
  99. velbusaio/module_spec/01.json +62 -0
  100. velbusaio/module_spec/02.json +16 -0
  101. velbusaio/module_spec/03.json +23 -0
  102. velbusaio/module_spec/04.json +283 -0
  103. velbusaio/module_spec/05.json +54 -0
  104. velbusaio/module_spec/06.json +110 -0
  105. velbusaio/module_spec/07.json +16 -0
  106. velbusaio/module_spec/08.json +38 -0
  107. velbusaio/module_spec/09.json +30 -0
  108. velbusaio/module_spec/0A.json +58 -0
  109. velbusaio/module_spec/0B.json +58 -0
  110. velbusaio/module_spec/0C.json +18 -0
  111. velbusaio/module_spec/0E.json +25 -0
  112. velbusaio/module_spec/0F.json +16 -0
  113. velbusaio/module_spec/10.json +111 -0
  114. velbusaio/module_spec/11.json +111 -0
  115. velbusaio/module_spec/12.json +73 -0
  116. velbusaio/module_spec/13.json +4 -0
  117. velbusaio/module_spec/14.json +16 -0
  118. velbusaio/module_spec/15.json +83 -0
  119. velbusaio/module_spec/16.json +129 -0
  120. velbusaio/module_spec/17.json +129 -0
  121. velbusaio/module_spec/18.json +129 -0
  122. velbusaio/module_spec/1A.json +79 -0
  123. velbusaio/module_spec/1B.json +107 -0
  124. velbusaio/module_spec/1D.json +89 -0
  125. velbusaio/module_spec/1E.json +306 -0
  126. velbusaio/module_spec/1F.json +178 -0
  127. velbusaio/module_spec/20.json +178 -0
  128. velbusaio/module_spec/21.json +326 -0
  129. velbusaio/module_spec/22.json +426 -0
  130. velbusaio/module_spec/23.json +129 -0
  131. velbusaio/module_spec/24.json +30 -0
  132. velbusaio/module_spec/25.json +3 -0
  133. velbusaio/module_spec/28.json +454 -0
  134. velbusaio/module_spec/29.json +235 -0
  135. velbusaio/module_spec/2A.json +239 -0
  136. velbusaio/module_spec/2B.json +239 -0
  137. velbusaio/module_spec/2C.json +257 -0
  138. velbusaio/module_spec/2D.json +270 -0
  139. velbusaio/module_spec/2E.json +215 -0
  140. velbusaio/module_spec/2F.json +211 -0
  141. velbusaio/module_spec/30.json +58 -0
  142. velbusaio/module_spec/31.json +465 -0
  143. velbusaio/module_spec/32.json +385 -0
  144. velbusaio/module_spec/33.json +249 -0
  145. velbusaio/module_spec/34.json +313 -0
  146. velbusaio/module_spec/35.json +313 -0
  147. velbusaio/module_spec/36.json +313 -0
  148. velbusaio/module_spec/37.json +333 -0
  149. velbusaio/module_spec/38.json +111 -0
  150. velbusaio/module_spec/39.json +4 -0
  151. velbusaio/module_spec/3A.json +306 -0
  152. velbusaio/module_spec/3B.json +306 -0
  153. velbusaio/module_spec/3C.json +306 -0
  154. velbusaio/module_spec/3D.json +454 -0
  155. velbusaio/module_spec/3E.json +302 -0
  156. velbusaio/module_spec/3F.json +4 -0
  157. velbusaio/module_spec/40.json +4 -0
  158. velbusaio/module_spec/41.json +241 -0
  159. velbusaio/module_spec/42.json +4 -0
  160. velbusaio/module_spec/43.json +23 -0
  161. velbusaio/module_spec/44.json +38 -0
  162. velbusaio/module_spec/45.json +4 -0
  163. velbusaio/module_spec/48.json +111 -0
  164. velbusaio/module_spec/49.json +111 -0
  165. velbusaio/module_spec/4A.json +89 -0
  166. velbusaio/module_spec/4B.json +138 -0
  167. velbusaio/module_spec/4C.json +129 -0
  168. velbusaio/module_spec/4D.json +108 -0
  169. velbusaio/module_spec/4E.json +787 -0
  170. velbusaio/module_spec/4F.json +114 -0
  171. velbusaio/module_spec/50.json +114 -0
  172. velbusaio/module_spec/51.json +114 -0
  173. velbusaio/module_spec/52.json +456 -0
  174. velbusaio/module_spec/54.json +270 -0
  175. velbusaio/module_spec/55.json +270 -0
  176. velbusaio/module_spec/56.json +270 -0
  177. velbusaio/module_spec/57.json +260 -0
  178. velbusaio/module_spec/5A.json +4 -0
  179. velbusaio/module_spec/5B.json +4 -0
  180. velbusaio/module_spec/5C.json +90 -0
  181. velbusaio/module_spec/5F.json +78 -0
  182. velbusaio/module_spec/60.json +4 -0
  183. velbusaio/module_spec/61.json +89 -0
  184. velbusaio/module_spec/broadcast.json +67 -0
  185. velbusaio/module_spec/ignore.json +22 -0
  186. velbusaio/protocol.py +34 -17
  187. velbusaio/raw_message.py +6 -6
  188. velbusaio/util.py +4 -0
  189. velbusaio/vlp_reader.py +249 -0
  190. velbus_aio-2023.12.0.dist-info/RECORD +0 -103
  191. velbusaio/moduleprotocol/protocol.json +0 -26507
  192. {velbus_aio-2023.12.0.dist-info → velbus_aio-2025.11.0.dist-info/licenses}/LICENSE +0 -0
velbusaio/handler.py CHANGED
@@ -2,21 +2,30 @@
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
 
7
8
  import asyncio
9
+ import importlib.resources
8
10
  import json
9
11
  import logging
10
- import re
11
- from typing import TYPE_CHECKING, Awaitable, Callable
12
+ import os
13
+ import pathlib
14
+ import sys
15
+ import time
16
+ from typing import TYPE_CHECKING
12
17
 
13
- import pkg_resources
18
+ from aiofile import async_open
14
19
 
15
20
  from velbusaio.command_registry import commandRegistry
16
- from velbusaio.helpers import h2, keys_exists
17
- 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
+ )
18
26
  from velbusaio.messages.module_subtype import ModuleSubTypeMessage
19
- from velbusaio.messages.module_type import ModuleTypeMessage
27
+ from velbusaio.messages.module_type import ModuleType2Message, ModuleTypeMessage
28
+ from velbusaio.module import Module
20
29
  from velbusaio.raw_message import RawMessage
21
30
 
22
31
  if TYPE_CHECKING:
@@ -25,32 +34,189 @@ if TYPE_CHECKING:
25
34
 
26
35
  class PacketHandler:
27
36
  """
28
- The packetHandler class
37
+ The PacketHandler class
29
38
  """
30
39
 
31
40
  def __init__(
32
41
  self,
33
- writer: Callable[[Message], Awaitable[None]],
34
42
  velbus: Velbus,
43
+ one_address: int | None = None,
35
44
  ) -> None:
36
- self._log = logging.getLogger("velbus-packet")
37
- self._writer = writer
45
+ self._log = logging.getLogger("velbus-handler")
46
+ self._log.setLevel(logging.DEBUG)
38
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
39
53
  self._scan_complete = False
40
- self._scan_complete_event = asyncio.Event()
41
- with open(
42
- pkg_resources.resource_filename(__name__, "moduleprotocol/protocol.json")
43
- ) as protocol_file:
44
- self.pdata = json.load(protocol_file)
45
-
46
- def scan_finished(self) -> None:
47
- self._scan_complete = True
48
- self._scan_complete_event.set()
49
- self._log.debug("Scan complete")
50
-
51
- def scan_started(self) -> None:
52
- self._scan_complete = False
53
- self._scan_complete_event.clear()
54
+ self._scan_delay_msec = 0
55
+ self.__scan_found_addresses: dict[int, ModuleTypeMessage | None] | None = None
56
+
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())
86
+
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")
201
+
202
+ async def __handle_module_type_response_async(self, rawmsg: RawMessage) -> None:
203
+ """
204
+ Handle a received module type response packet
205
+ """
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
54
220
 
55
221
  async def handle(self, rawmsg: RawMessage) -> None:
56
222
  """
@@ -67,153 +233,119 @@ class PacketHandler:
67
233
  command_value = rawmsg.command
68
234
  data = rawmsg.data_only
69
235
 
70
- if command_value == 0xFF and not self._scan_complete:
71
- msg = ModuleTypeMessage()
72
- msg.populate(priority, address, rtr, data)
73
- self._log.debug(f"Received {msg}")
74
- await self._handle_module_type(msg)
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
75
241
  elif command_value in (0xB0, 0xA7, 0xA6) and not self._scan_complete:
76
- msg = ModuleSubTypeMessage()
242
+ msg: ModuleSubTypeMessage = ModuleSubTypeMessage()
77
243
  msg.populate(priority, address, rtr, data)
78
-
79
244
  if command_value == 0xB0:
80
245
  msg.sub_address_offset = 0
81
246
  elif command_value == 0xA7:
82
247
  msg.sub_address_offset = 4
83
248
  elif command_value == 0xA6:
84
249
  msg.sub_address_offset = 8
85
- else:
86
- raise RuntimeError("Unreachable code reached => bug here")
250
+ async with self._scanLock:
251
+ self._scan_delay_msec += SCAN_MODULEINFO_TIMEOUT_INTERVAL
252
+ self._handle_module_subtype(msg)
87
253
 
88
- self._log.debug(f"Received {msg}")
89
- await self._handle_module_subtype(msg)
90
- elif command_value in self.pdata["MessagesBroadCast"]:
254
+ # ignore broadcast
255
+ elif command_value in self.broadcast:
91
256
  self._log.debug(
92
257
  "Received broadcast message {} from {}, ignoring".format(
93
- self.pdata["MessageBroadCast"][command_value.upper()], address
258
+ self.broadcast[str(command_value).upper()], address
94
259
  )
95
260
  )
96
- elif address in self._velbus.get_modules().keys():
97
- module_type = self._velbus.get_module(address).get_type()
98
- if commandRegistry.has_command(int(command_value), module_type):
99
- command = commandRegistry.get_command(command_value, module_type)
100
- msg = command()
101
- msg.populate(priority, address, rtr, data)
102
- self._log.debug(f"Received {msg}")
103
- # send the message to the modules
104
- await self._velbus.get_module(msg.address).on_message(msg)
105
- else:
106
- self._log.warning(
107
- "NOT FOUND IN command_registry: addr={} cmd={} packet={}".format(
108
- address, command_value, ":".join(format(x, "02x") for x in data)
109
- )
110
- )
111
- elif self._scan_complete:
112
- # this should only happen once the scan is complete, of its not complete suspended the error message
113
- self._log.warning(
114
- "UNKNOWN module, you should initialize a full new velbus scan: packet={}, address={}, modules={}".format(
115
- ":".join(format(x, "02x") for x in data),
116
- address,
117
- 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
118
267
  )
119
268
  )
120
269
 
121
- # def _handle_message(self, rawMsg: RawMessage) -> None:
122
- # module_type = self._velbus.get_module(rawMsg.address).get_type()
123
- # this_msg = keys_exists(
124
- # self.pdata, "ModuleTypes", h2(module_type), "Messages", h2(rawMsg.command), "Data"
125
- # )
126
- # if this_msg and "PerByte" in this_msg:
127
- # self._per_byte(this_msg["PerByte"], rawMsg)
128
-
129
- # def _per_byte(self, cmsg, rawMsg: RawMessage) -> dict:
130
- # result = {}
131
- # byte_index = 0
132
- # for byte in rawMsg.data:
133
- # num = str(byte_index)
134
- # # only do something if its defined
135
- # if num not in cmsg:
136
- # continue
137
- # # check if we can do a binary match
138
- # for mat in cmsg[num]["Match"]:
139
- # if (
140
- # (mat.startswith("%") and re.match(mat[1:], f"{byte:08b}"))
141
- # or mat == f"{byte:08b}"
142
- # or mat == f"{byte:02x}"
143
- # ):
144
- # result = self._per_byte_handle(
145
- # result, cmsg[num]["Match"][mat], byte
146
- # )
147
- # byte_index += 1
148
- # return result
149
-
150
- # def _per_byte_handle(self, result: dict, todo: dict, byte: int) -> dict:
151
- # if "Channel" in todo:
152
- # result["Channel"] = todo["Channel"]
153
- # if "Value" in todo:
154
- # result["Value"] = todo["Value"]
155
- # if "Convert" in todo:
156
- # result["ValueList"] = []
157
- # if todo["Convert"] == "Decimal":
158
- # result["ValueList"].append(int(byte))
159
- # elif todo["Convert"] == "Counter":
160
- # result["ValueList"].append(f"{byte:02x}")
161
- # elif todo["Convert"] == "Temperature":
162
- # print("CONVERT temperature")
163
- # elif todo["Convert"] == "Divider":
164
- # bin_str = f"{byte:08b}"
165
- # chan = bin_str[6:]
166
- # val = bin_str[:5]
167
- # print(f"CONVERT Divider {chan} {val}")
168
- # elif todo["Convert"] == "Channel":
169
- # print("CONVERT Channel")
170
- # elif todo["Convert"] == "ChannelBit":
171
- # print("CONVERT ChannelBit")
172
- # elif todo["Convert"].startswith("ChannelBitStatus"):
173
- # print("CONVERT ChannelBitStatus")
174
- # else:
175
- # self._log.error("UNKNOWN convert requested: {}".format(todo["Convert"]))
176
- # return result
177
-
178
- async def _handle_module_type(self, msg: Message) -> None:
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}")
298
+
299
+ async def _handle_module_type(
300
+ self, msg: ModuleTypeMessage | ModuleType2Message
301
+ ) -> None:
179
302
  """
180
303
  load the module data
181
304
  """
182
- data = keys_exists(self.pdata, "ModuleTypes", h2(msg.module_type))
183
- if not data:
184
- self._log.warning(f"Module not recognized: {msg.module_type}")
185
- return
186
- # create the module
187
- await self._velbus.add_module(
188
- msg.address,
189
- msg.module_type,
190
- data,
191
- memorymap=msg.memory_map_version,
192
- build_year=msg.build_year,
193
- build_week=msg.build_week,
194
- serial=msg.serial,
195
- )
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
+ )
196
324
 
197
- async def _handle_module_subtype(self, msg: Message) -> None:
198
- if msg.address not in self._velbus.get_modules():
199
- return
200
- addrList = {
201
- (msg.sub_address_offset + 1): msg.sub_address_1,
202
- (msg.sub_address_offset + 2): msg.sub_address_2,
203
- (msg.sub_address_offset + 3): msg.sub_address_3,
204
- (msg.sub_address_offset + 4): msg.sub_address_4,
205
- }
206
- await self._velbus.add_submodules(msg.address, addrList)
207
-
208
- def _channel_convert(self, module: str, channel: str, ctype: str) -> None | int:
209
- data = keys_exists(
210
- self.pdata, "ModuleTypes", h2(module), "ChannelNumbers", ctype
211
- )
212
- if data and "Map" in data and h2(channel) in data["Map"]:
213
- return data["Map"][h2(channel)]
214
- if data and "Convert" in data:
215
- return int(channel)
216
- for offset in range(0, 8):
217
- if channel & (1 << offset):
218
- return offset + 1
219
- 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,11 +1,12 @@
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
8
- from typing import Any, Dict
9
+ from typing import Any
9
10
 
10
11
  from velbusaio.const import CACHEDIR
11
12
 
velbusaio/message.py CHANGED
@@ -1,10 +1,11 @@
1
1
  """
2
2
  The velbus abstract message class
3
3
  """
4
+
4
5
  from __future__ import annotations
5
6
 
7
+ import enum
6
8
  import json
7
- from typing import Optional
8
9
 
9
10
  from velbusaio.const import PRIORITY_FIRMWARE, PRIORITY_HIGH, PRIORITY_LOW
10
11
 
@@ -20,19 +21,19 @@ class Message:
20
21
  Base Velbus message
21
22
  """
22
23
 
23
- def __init__(self, address: int = None) -> None:
24
+ def __init__(self, address: int = 0) -> None:
24
25
  self.priority = PRIORITY_LOW
25
- self.address = None
26
- self.rtr = False
26
+ self.address: int = 0
27
+ self.rtr: bool = False
27
28
  self.data = bytearray()
28
29
  self.set_defaults(address)
29
30
 
30
- def set_attributes(self, priority: int, address: int, rtr: int) -> None:
31
+ def set_attributes(self, priority: int, address: int, rtr: bool) -> None:
31
32
  self.priority = priority
32
33
  self.address = address
33
34
  self.rtr = rtr
34
35
 
35
- def populate(self, priority: int, address: int, rtr: int, data: int) -> None:
36
+ def populate(self, priority: int, address: int, rtr: bool, data: int) -> None:
36
37
  raise NotImplementedError
37
38
 
38
39
  def set_defaults(self, address: int | None) -> None:
@@ -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
- me[key] = str(me[key], "utf-8")
69
+ if isinstance(me[key], (bytes, bytearray, enum.Enum)):
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:
@@ -1,6 +1,7 @@
1
1
  """
2
2
  :author: Thomas Delaet <thomas@delaet.org>
3
3
  """
4
+
4
5
  from __future__ import annotations
5
6
 
6
7
  from velbusaio.messages.blind_status import BlindStatusMessage, BlindStatusNgMessage
@@ -32,6 +33,7 @@ from velbusaio.messages.channel_name_request import (
32
33
  from velbusaio.messages.clear_led import ClearLedMessage
33
34
  from velbusaio.messages.counter_status import CounterStatusMessage
34
35
  from velbusaio.messages.counter_status_request import CounterStatusRequestMessage
36
+ from velbusaio.messages.counter_value import CounterValueMessage
35
37
  from velbusaio.messages.cover_down import CoverDownMessage, CoverDownMessage2
36
38
  from velbusaio.messages.cover_off import CoverOffMessage, CoverOffMessage2
37
39
  from velbusaio.messages.cover_position import CoverPosMessage
@@ -50,13 +52,15 @@ from velbusaio.messages.memo_text import MemoTextMessage
50
52
  from velbusaio.messages.memory_data import MemoryDataMessage
51
53
  from velbusaio.messages.memory_data_block import MemoryDataBlockMessage
52
54
  from velbusaio.messages.memory_dump_request import MemoryDumpRequestMessage
53
- from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
54
55
  from velbusaio.messages.module_status import ModuleStatusMessage, ModuleStatusMessage2
55
56
  from velbusaio.messages.module_status_request import ModuleStatusRequestMessage
56
57
  from velbusaio.messages.module_subtype import ModuleSubTypeMessage
57
- from velbusaio.messages.module_type import ModuleTypeMessage, ModuleType2Message
58
+ from velbusaio.messages.module_type import ModuleType2Message, ModuleTypeMessage
58
59
  from velbusaio.messages.module_type_request import ModuleTypeRequestMessage
60
+ from velbusaio.messages.psu_load import PsuLoadMessage
61
+ from velbusaio.messages.psu_values import PsuValuesMessage
59
62
  from velbusaio.messages.push_button_status import PushButtonStatusMessage
63
+ from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
60
64
  from velbusaio.messages.read_data_block_from_memory import (
61
65
  ReadDataBlockFromMemoryMessage,
62
66
  )
@@ -1,6 +1,7 @@
1
1
  """
2
2
  :author: Tom Dupré <gitd8400@gmail.com>
3
3
  """
4
+
4
5
  from __future__ import annotations
5
6
 
6
7
  import json
@@ -12,7 +13,7 @@ COMMAND_CODE = 0xEC
12
13
  DSTATUS = {0: "off", 1: "up", 2: "down"}
13
14
 
14
15
 
15
- @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS"])
16
+ @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS", "VMB2BLE-10"])
16
17
  class BlindStatusNgMessage(Message):
17
18
  """
18
19
  sent by: VMB2BLE
@@ -38,7 +39,7 @@ class BlindStatusNgMessage(Message):
38
39
  self.channel = self.byte_to_channel(data[0])
39
40
  self.timeout = data[1] # Omzetter seconden ????
40
41
  self.status = data[2]
41
- self.position = data[4]
42
+ self.position = data[4] # 0..255 (0=open, 255=closed)
42
43
 
43
44
  def to_json(self):
44
45
  """
@@ -47,6 +48,7 @@ class BlindStatusNgMessage(Message):
47
48
  json_dict = self.to_json_basic()
48
49
  json_dict["channel"] = self.channel
49
50
  json_dict["timeout"] = self.timeout
51
+ json_dict["position"] = self.position
50
52
  json_dict["status"] = DSTATUS[self.status]
51
53
  return json.dumps(json_dict)
52
54
 
@@ -1,6 +1,7 @@
1
1
  """
2
2
  :author: Thomas Delaet <thomas@delaet.org>
3
3
  """
4
+
4
5
  from __future__ import annotations
5
6
 
6
7
  from velbusaio.command_registry import register
@@ -1,6 +1,7 @@
1
1
  """
2
2
  :author: Thomas Delaet <thomas@delaet.org>
3
3
  """
4
+
4
5
  from __future__ import annotations
5
6
 
6
7
  from velbusaio.command_registry import register
@@ -1,6 +1,7 @@
1
1
  """
2
2
  :author: Thomas Delaet <thomas@delaet.org>
3
3
  """
4
+
4
5
  from __future__ import annotations
5
6
 
6
7
  from velbusaio.command_registry import register