velbus-aio 2024.7.6__py3-none-any.whl → 2024.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.

Potentially problematic release.


This version of velbus-aio might be problematic. Click here for more details.

Files changed (105) hide show
  1. {velbus_aio-2024.7.6.dist-info → velbus_aio-2024.11.0.dist-info}/METADATA +17 -5
  2. velbus_aio-2024.11.0.dist-info/RECORD +183 -0
  3. {velbus_aio-2024.7.6.dist-info → velbus_aio-2024.11.0.dist-info}/WHEEL +1 -1
  4. velbusaio/channels.py +16 -4
  5. velbusaio/command_registry.py +7 -5
  6. velbusaio/const.py +1 -1
  7. velbusaio/controller.py +8 -5
  8. velbusaio/handler.py +87 -61
  9. velbusaio/messages/__init__.py +2 -2
  10. velbusaio/messages/blind_status.py +1 -1
  11. velbusaio/messages/channel_name_part1.py +1 -0
  12. velbusaio/messages/channel_name_part2.py +1 -0
  13. velbusaio/messages/channel_name_part3.py +1 -0
  14. velbusaio/messages/cover_down.py +1 -1
  15. velbusaio/messages/cover_off.py +1 -1
  16. velbusaio/messages/cover_position.py +1 -1
  17. velbusaio/messages/cover_up.py +1 -1
  18. velbusaio/messages/edge_set_color.py +1 -0
  19. velbusaio/module.py +77 -29
  20. velbusaio/module_spec/01.json +50 -0
  21. velbusaio/module_spec/02.json +11 -0
  22. velbusaio/module_spec/03.json +18 -0
  23. velbusaio/module_spec/05.json +42 -0
  24. velbusaio/module_spec/06.json +105 -0
  25. velbusaio/module_spec/07.json +11 -0
  26. velbusaio/module_spec/08.json +30 -0
  27. velbusaio/module_spec/09.json +24 -0
  28. velbusaio/module_spec/0A.json +46 -0
  29. velbusaio/module_spec/0B.json +46 -0
  30. velbusaio/module_spec/0C.json +13 -0
  31. velbusaio/module_spec/0E.json +25 -0
  32. velbusaio/module_spec/0F.json +11 -0
  33. velbusaio/module_spec/10.json +104 -0
  34. velbusaio/module_spec/11.json +104 -0
  35. velbusaio/module_spec/12.json +67 -0
  36. velbusaio/module_spec/13.json +4 -0
  37. velbusaio/module_spec/14.json +11 -0
  38. velbusaio/module_spec/15.json +80 -0
  39. velbusaio/module_spec/16.json +119 -0
  40. velbusaio/module_spec/17.json +119 -0
  41. velbusaio/module_spec/18.json +119 -0
  42. velbusaio/module_spec/1A.json +73 -0
  43. velbusaio/module_spec/1B.json +100 -0
  44. velbusaio/module_spec/1D.json +83 -0
  45. velbusaio/module_spec/1E.json +295 -0
  46. velbusaio/module_spec/1F.json +167 -0
  47. velbusaio/module_spec/20.json +167 -0
  48. velbusaio/module_spec/21.json +291 -0
  49. velbusaio/module_spec/22.json +330 -0
  50. velbusaio/module_spec/23.json +129 -0
  51. velbusaio/module_spec/25.json +3 -0
  52. velbusaio/module_spec/28.json +419 -0
  53. velbusaio/module_spec/29.json +228 -0
  54. velbusaio/module_spec/2A.json +239 -0
  55. velbusaio/module_spec/2B.json +239 -0
  56. velbusaio/module_spec/2C.json +257 -0
  57. velbusaio/module_spec/2D.json +295 -0
  58. velbusaio/module_spec/2E.json +212 -0
  59. velbusaio/module_spec/2F.json +208 -0
  60. velbusaio/module_spec/30.json +46 -0
  61. velbusaio/module_spec/31.json +465 -0
  62. velbusaio/module_spec/32.json +365 -0
  63. velbusaio/module_spec/33.json +239 -0
  64. velbusaio/module_spec/34.json +157 -0
  65. velbusaio/module_spec/35.json +157 -0
  66. velbusaio/module_spec/36.json +157 -0
  67. velbusaio/module_spec/37.json +297 -0
  68. velbusaio/module_spec/38.json +102 -0
  69. velbusaio/module_spec/39.json +4 -0
  70. velbusaio/module_spec/3A.json +295 -0
  71. velbusaio/module_spec/3B.json +295 -0
  72. velbusaio/module_spec/3C.json +295 -0
  73. velbusaio/module_spec/3D.json +419 -0
  74. velbusaio/module_spec/3E.json +295 -0
  75. velbusaio/module_spec/3F.json +4 -0
  76. velbusaio/module_spec/40.json +4 -0
  77. velbusaio/module_spec/41.json +233 -0
  78. velbusaio/module_spec/42.json +4 -0
  79. velbusaio/module_spec/43.json +11 -0
  80. velbusaio/module_spec/44.json +26 -0
  81. velbusaio/module_spec/45.json +4 -0
  82. velbusaio/module_spec/48.json +104 -0
  83. velbusaio/module_spec/49.json +104 -0
  84. velbusaio/module_spec/4A.json +83 -0
  85. velbusaio/module_spec/4B.json +126 -0
  86. velbusaio/module_spec/4C.json +119 -0
  87. velbusaio/module_spec/4D.json +108 -0
  88. velbusaio/module_spec/4E.json +451 -0
  89. velbusaio/module_spec/4F.json +89 -0
  90. velbusaio/module_spec/50.json +89 -0
  91. velbusaio/module_spec/51.json +89 -0
  92. velbusaio/module_spec/52.json +189 -0
  93. velbusaio/module_spec/54.json +4 -0
  94. velbusaio/module_spec/55.json +4 -0
  95. velbusaio/module_spec/56.json +4 -0
  96. velbusaio/module_spec/57.json +222 -0
  97. velbusaio/module_spec/5A.json +4 -0
  98. velbusaio/module_spec/5C.json +87 -0
  99. velbusaio/module_spec/5F.json +66 -0
  100. velbusaio/module_spec/broadcast.json +67 -0
  101. velbusaio/protocol.py +1 -1
  102. velbus_aio-2024.7.6.dist-info/RECORD +0 -103
  103. velbusaio/protocol.json +0 -10194
  104. {velbus_aio-2024.7.6.dist-info → velbus_aio-2024.11.0.dist-info}/LICENSE +0 -0
  105. {velbus_aio-2024.7.6.dist-info → velbus_aio-2024.11.0.dist-info}/top_level.txt +0 -0
velbusaio/handler.py CHANGED
@@ -5,30 +5,30 @@ Velbus packet handler
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from velbusaio.const import SCAN_MODULETYPE_TIMEOUT
9
- from velbusaio.const import SCAN_MODULEINFO_TIMEOUT_INITIAL
10
- from velbusaio.const import SCAN_MODULEINFO_TIMEOUT_INTERVAL
11
-
12
8
  import asyncio
9
+ import importlib.resources
13
10
  import json
14
11
  import logging
15
- import threading
16
12
  import os
17
13
  import pathlib
14
+ import pprint
15
+ import sys
16
+ from typing import TYPE_CHECKING, Awaitable, Callable
18
17
 
19
18
  from aiofile import async_open
20
19
 
21
- from typing import TYPE_CHECKING, Awaitable, Callable
22
- import pkg_resources
23
-
24
20
  from velbusaio.command_registry import commandRegistry
21
+ from velbusaio.const import (
22
+ SCAN_MODULEINFO_TIMEOUT_INITIAL,
23
+ SCAN_MODULEINFO_TIMEOUT_INTERVAL,
24
+ SCAN_MODULETYPE_TIMEOUT,
25
+ )
25
26
  from velbusaio.helpers import h2, keys_exists
26
27
  from velbusaio.message import Message
27
28
  from velbusaio.messages.module_subtype import ModuleSubTypeMessage
28
- from velbusaio.messages.module_type import ModuleTypeMessage, ModuleType2Message
29
+ from velbusaio.messages.module_type import ModuleType2Message, ModuleTypeMessage
29
30
  from velbusaio.raw_message import RawMessage
30
31
 
31
-
32
32
  if TYPE_CHECKING:
33
33
  from velbusaio.controller import Velbus
34
34
 
@@ -41,21 +41,34 @@ class PacketHandler:
41
41
  def __init__(
42
42
  self,
43
43
  velbus: Velbus,
44
+ one_address: int | None = None,
44
45
  ) -> None:
45
46
  self._log = logging.getLogger("velbus-handler")
46
47
  self._log.setLevel(logging.DEBUG)
47
48
  self._velbus = velbus
49
+ self._one_address = one_address
48
50
  self._typeResponseReceived = asyncio.Event()
49
- self._scanLock = threading.Lock()
51
+ self._scanLock = asyncio.Lock()
50
52
  self._modulescan_address = 0
51
53
  self._scan_complete = False
52
54
  self._scan_delay_msec = 0
53
55
 
54
56
  async def read_protocol_data(self):
55
- async with async_open(
56
- pkg_resources.resource_filename(__name__, "protocol.json")
57
- ) as protocol_file:
58
- self.pdata = json.loads(await protocol_file.read())
57
+ if sys.version_info >= (3, 13):
58
+ with importlib.resources.path(
59
+ __name__, "module_spec/broadcast.json"
60
+ ) as fspath:
61
+ async with async_open(fspath) as protocol_file:
62
+ self.broadcast = json.loads(await protocol_file.read())
63
+ else:
64
+ async with async_open(
65
+ str(
66
+ importlib.resources.files(__name__.split(".")[0]).joinpath(
67
+ "module_spec/broadcast.json"
68
+ )
69
+ )
70
+ ) as protocol_file:
71
+ self.broadcast = json.loads(await protocol_file.read())
59
72
 
60
73
  def empty_cache(self) -> bool:
61
74
  if (
@@ -84,12 +97,22 @@ class PacketHandler:
84
97
  while self._modulescan_address < 254:
85
98
  address = 0
86
99
  module = None
87
- with self._scanLock:
100
+ async with self._scanLock:
88
101
  self._modulescan_address = self._modulescan_address + 1
89
102
  address = self._modulescan_address
103
+ if self._velbus.addr_is_submodule(address):
104
+ self._log.info(
105
+ f"Skipping submodule address {address}, already handled"
106
+ )
107
+ continue
108
+ self._log.info(f"Starting handling scan {address}")
90
109
  module = self._velbus.get_module(address)
91
110
 
92
- self._log.info(f"Starting handling scan {address}")
111
+ if self._one_address is not None and address != int(self._one_address):
112
+ self._log.info(
113
+ f"Skipping address {address} as we requested to only scan one address {self._one_address}"
114
+ )
115
+ continue
93
116
 
94
117
  cfile = pathlib.Path(f"{self._velbus.get_cache_dir()}/{address}.json")
95
118
  # cleanup the old module cache if needed
@@ -107,7 +130,7 @@ class PacketHandler:
107
130
  self._typeResponseReceived.wait(),
108
131
  SCAN_MODULETYPE_TIMEOUT / 1000.0,
109
132
  )
110
- with self._scanLock:
133
+ async with self._scanLock:
111
134
  module = self._velbus.get_module(address)
112
135
  except asyncio.TimeoutError:
113
136
  self._log.info(
@@ -115,20 +138,24 @@ class PacketHandler:
115
138
  )
116
139
  if module is not None:
117
140
  try:
118
- self._log.debug(f"Module {address} detected: start loading")
141
+ self._log.debug(
142
+ f"Module {module._address} detected: start loading"
143
+ )
119
144
  await asyncio.wait_for(
120
145
  module.load(from_cache=True),
121
146
  SCAN_MODULEINFO_TIMEOUT_INITIAL / 1000.0,
122
147
  )
123
- self._scan_delay_msec = SCAN_MODULEINFO_TIMEOUT_INITIAL
124
- while self._scan_delay_msec > 50 and not module.is_loaded():
148
+ self._scan_delay_msec = module.get_initial_timeout()
149
+ while (
150
+ self._scan_delay_msec > 50 and not await module.is_loaded()
151
+ ):
125
152
  # self._log.debug(
126
- # f"\t... waiting {self._scan_delay_msec} is_loaded={module.is_loaded()}"
153
+ # f"\t... waiting {self._scan_delay_msec} is_loaded={await module.is_loaded()}"
127
154
  # )
128
155
  self._scan_delay_msec = self._scan_delay_msec - 50
129
156
  await asyncio.sleep(0.05)
130
157
  self._log.info(
131
- f"Scan module {address} completed, module loaded={module.is_loaded()}"
158
+ f"Scan module {address} completed, module loaded={await module.is_loaded()}"
132
159
  )
133
160
  except asyncio.TimeoutError:
134
161
  self._log.error(
@@ -153,49 +180,47 @@ class PacketHandler:
153
180
  data = rawmsg.data_only
154
181
 
155
182
  # handle module type response message
156
- if command_value == 0xFF:
157
- if not self._scan_complete:
158
- tmsg: ModuleTypeMessage = ModuleTypeMessage()
159
- tmsg.populate(priority, address, rtr, data)
160
- with self._scanLock:
161
- self._handle_module_type(tmsg)
162
- if address == self._modulescan_address:
163
- self._typeResponseReceived.set()
164
- else:
165
- self._log.debug(
166
- f"Unexpected module type message module address {address}, Velbuslink scan?"
167
- )
168
- self._modulescan_address = address - 1
183
+ if command_value == 0xFF and not self._scan_complete:
184
+ tmsg: ModuleTypeMessage = ModuleTypeMessage()
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
169
195
 
170
- self._typeResponseReceived.set()
196
+ self._typeResponseReceived.set()
171
197
 
172
198
  # handle module subtype response message
173
- elif command_value in (0xB0, 0xA7, 0xA6):
174
- if not self._scan_complete:
175
- msg: ModuleSubTypeMessage = ModuleSubTypeMessage()
176
- msg.populate(priority, address, rtr, data)
177
- if command_value == 0xB0:
178
- msg.sub_address_offset = 0
179
- elif command_value == 0xA7:
180
- msg.sub_address_offset = 4
181
- elif command_value == 0xA6:
182
- msg.sub_address_offset = 8
183
- with self._scanLock:
184
- self._scan_delay_msec = SCAN_MODULEINFO_TIMEOUT_INITIAL
185
- self._handle_module_subtype(msg)
199
+ elif command_value in (0xB0, 0xA7, 0xA6) and not self._scan_complete:
200
+ msg: ModuleSubTypeMessage = ModuleSubTypeMessage()
201
+ msg.populate(priority, address, rtr, data)
202
+ if command_value == 0xB0:
203
+ msg.sub_address_offset = 0
204
+ elif command_value == 0xA7:
205
+ msg.sub_address_offset = 4
206
+ elif command_value == 0xA6:
207
+ msg.sub_address_offset = 8
208
+ async with self._scanLock:
209
+ self._scan_delay_msec += SCAN_MODULEINFO_TIMEOUT_INTERVAL
210
+ self._handle_module_subtype(msg)
186
211
 
187
212
  # ignore broadcast
188
- elif command_value in self.pdata["MessagesBroadCast"]:
213
+ elif command_value in self.broadcast:
189
214
  self._log.debug(
190
215
  "Received broadcast message {} from {}, ignoring".format(
191
- self.pdata["MessageBroadCast"][str(command_value).upper()], address
216
+ self.broadcast[str(command_value).upper()], address
192
217
  )
193
218
  )
194
219
 
195
220
  # handle other messages for modules that are already scanned
196
221
  else:
197
222
  module = None
198
- with self._scanLock:
223
+ async with self._scanLock:
199
224
  module = self._velbus.get_module(address)
200
225
  if module is not None:
201
226
  module_type = module.get_type()
@@ -214,28 +239,29 @@ class PacketHandler:
214
239
  0xFE,
215
240
  0xCC,
216
241
  ): # names, memory data, memory block
217
- self._scan_delay_msec = SCAN_MODULEINFO_TIMEOUT_INTERVAL
242
+ self._scan_delay_msec += SCAN_MODULEINFO_TIMEOUT_INTERVAL
218
243
  # self._log.debug(f"Restart timeout {msg}")
219
244
  # send the message to the modules
220
245
  await module.on_message(msg)
221
246
  else:
222
247
  self._log.warning(f"NOT FOUND IN command_registry: {rawmsg}")
223
248
 
224
- def _handle_module_type(self, msg: ModuleTypeMessage | ModuleType2Message) -> None:
249
+ async def _handle_module_type(
250
+ self, msg: ModuleTypeMessage | ModuleType2Message
251
+ ) -> None:
225
252
  """
226
253
  load the module data
227
254
  """
228
255
  if msg is not None:
229
256
  module = self._velbus.get_module(msg.address)
230
257
  if module is None:
231
- data = keys_exists(self.pdata, "ModuleTypes", h2(msg.module_type))
232
- if not data:
233
- self._log.warning(f"Module not recognized: {msg.module_type}")
234
- return
235
- self._velbus.add_module(
258
+ # data = keys_exists(self.pdata, "ModuleTypes", h2(msg.module_type))
259
+ # if not data:
260
+ # self._log.warning(f"Module not recognized: {msg.module_type}")
261
+ # return
262
+ await self._velbus.add_module(
236
263
  msg.address,
237
264
  msg.module_type,
238
- data,
239
265
  memorymap=msg.memory_map_version,
240
266
  build_year=msg.build_year,
241
267
  build_week=msg.build_week,
@@ -51,13 +51,13 @@ from velbusaio.messages.memo_text import MemoTextMessage
51
51
  from velbusaio.messages.memory_data import MemoryDataMessage
52
52
  from velbusaio.messages.memory_data_block import MemoryDataBlockMessage
53
53
  from velbusaio.messages.memory_dump_request import MemoryDumpRequestMessage
54
- from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
55
54
  from velbusaio.messages.module_status import ModuleStatusMessage, ModuleStatusMessage2
56
55
  from velbusaio.messages.module_status_request import ModuleStatusRequestMessage
57
56
  from velbusaio.messages.module_subtype import ModuleSubTypeMessage
58
- from velbusaio.messages.module_type import ModuleTypeMessage, ModuleType2Message
57
+ from velbusaio.messages.module_type import ModuleType2Message, ModuleTypeMessage
59
58
  from velbusaio.messages.module_type_request import ModuleTypeRequestMessage
60
59
  from velbusaio.messages.push_button_status import PushButtonStatusMessage
60
+ from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
61
61
  from velbusaio.messages.read_data_block_from_memory import (
62
62
  ReadDataBlockFromMemoryMessage,
63
63
  )
@@ -13,7 +13,7 @@ COMMAND_CODE = 0xEC
13
13
  DSTATUS = {0: "off", 1: "up", 2: "down"}
14
14
 
15
15
 
16
- @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS"])
16
+ @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS", "VMB2BLE-10"])
17
17
  class BlindStatusNgMessage(Message):
18
18
  """
19
19
  sent by: VMB2BLE
@@ -84,6 +84,7 @@ class ChannelNamePart1Message(Message):
84
84
  "VMBGP4PIR-20",
85
85
  "VMB4LEDPWM-20",
86
86
  "VMB8DC-20",
87
+ "VMB8IN-20",
87
88
  ],
88
89
  )
89
90
  class ChannelNamePart1Message2(ChannelNamePart1Message):
@@ -83,6 +83,7 @@ class ChannelNamePart2Message(Message):
83
83
  "VMBEL4PIR-20",
84
84
  "VMBGP4PIR-20",
85
85
  "VMB8DC-20",
86
+ "VMB8IN-20",
86
87
  ],
87
88
  )
88
89
  class ChannelNamePart2Message2(ChannelNamePart2Message):
@@ -83,6 +83,7 @@ class ChannelNamePart3Message(Message):
83
83
  "VMBEL4PIR-20",
84
84
  "VMBGP4PIR-20",
85
85
  "VMB8DC-20",
86
+ "VMB8IN-20",
86
87
  ],
87
88
  )
88
89
  class ChannelNamePart3Message2(ChannelNamePart3Message):
@@ -12,7 +12,7 @@ from velbusaio.message import Message
12
12
  COMMAND_CODE = 0x06
13
13
 
14
14
 
15
- @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS"])
15
+ @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS", "VMB2BLE-10"])
16
16
  class CoverDownMessage(Message):
17
17
  """
18
18
  sent by:
@@ -10,7 +10,7 @@ from velbusaio.message import Message
10
10
  COMMAND_CODE = 0x04
11
11
 
12
12
 
13
- @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS"])
13
+ @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS", "VMB2BLE-10"])
14
14
  class CoverOffMessage(Message):
15
15
  """
16
16
  sent by:
@@ -10,7 +10,7 @@ from velbusaio.message import Message
10
10
  COMMAND_CODE = 0x1C
11
11
 
12
12
 
13
- @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS"])
13
+ @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS", "VMB2BLE-10"])
14
14
  class CoverPosMessage(Message):
15
15
  """
16
16
  sent by:
@@ -12,7 +12,7 @@ from velbusaio.message import Message
12
12
  COMMAND_CODE = 0x05
13
13
 
14
14
 
15
- @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS"])
15
+ @register(COMMAND_CODE, ["VMB1BLE", "VMB2BLE", "VMB1BLS", "VMB2BLE-10"])
16
16
  class CoverUpMessage(Message):
17
17
  """
18
18
  sent by:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from enum import IntEnum
4
+
4
5
  from velbusaio.command_registry import register
5
6
  from velbusaio.message import Message
6
7
 
velbusaio/module.py CHANGED
@@ -4,14 +4,17 @@ This represents a velbus module
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import importlib.resources
8
+ import json
7
9
  import logging
10
+ import os
8
11
  import pathlib
9
12
  import struct
10
13
  import sys
11
- import json
12
- import os
13
14
  from typing import Awaitable, Callable
14
15
 
16
+ from aiofile import async_open
17
+
15
18
  from velbusaio.channels import (
16
19
  Blind,
17
20
  Button,
@@ -25,19 +28,20 @@ from velbusaio.channels import (
25
28
  SelectedProgram,
26
29
  Sensor,
27
30
  SensorNumber,
28
- Temperature,
29
- ThermostatChannel,
30
31
  )
32
+ from velbusaio.channels import Temperature
33
+ from velbusaio.channels import Temperature as TemperatureChannelType
34
+ from velbusaio.channels import ThermostatChannel
31
35
  from velbusaio.command_registry import commandRegistry
32
36
  from velbusaio.const import (
33
37
  CHANNEL_LIGHT_VALUE,
34
38
  CHANNEL_MEMO_TEXT,
35
39
  CHANNEL_SELECTED_PROGRAM,
36
40
  PRIORITY_LOW,
41
+ SCAN_MODULEINFO_TIMEOUT_INITIAL,
37
42
  )
38
- from velbusaio.helpers import handle_match, keys_exists
43
+ from velbusaio.helpers import h2, handle_match, keys_exists
39
44
  from velbusaio.message import Message
40
- from velbusaio.messages.dali_device_settings import DaliDeviceSettingMsg
41
45
  from velbusaio.messages.blind_status import BlindStatusMessage, BlindStatusNgMessage
42
46
  from velbusaio.messages.channel_name_part1 import (
43
47
  ChannelNamePart1Message,
@@ -61,6 +65,7 @@ from velbusaio.messages.channel_name_request import ChannelNameRequestMessage
61
65
  from velbusaio.messages.clear_led import ClearLedMessage
62
66
  from velbusaio.messages.counter_status import CounterStatusMessage
63
67
  from velbusaio.messages.counter_status_request import CounterStatusRequestMessage
68
+ from velbusaio.messages.dali_device_settings import DaliDeviceSettingMsg
64
69
  from velbusaio.messages.dali_device_settings import DeviceType as DaliDeviceType
65
70
  from velbusaio.messages.dali_device_settings import DeviceTypeMsg as DaliDeviceTypeMsg
66
71
  from velbusaio.messages.dali_device_settings import MemberOfGroupMsg
@@ -73,7 +78,6 @@ from velbusaio.messages.dimmer_channel_status import DimmerChannelStatusMessage
73
78
  from velbusaio.messages.dimmer_status import DimmerStatusMessage
74
79
  from velbusaio.messages.fast_blinking_led import FastBlinkingLedMessage
75
80
  from velbusaio.messages.memory_data import MemoryDataMessage
76
- from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
77
81
  from velbusaio.messages.module_status import (
78
82
  ModuleStatusGP4PirMessage,
79
83
  ModuleStatusMessage,
@@ -82,6 +86,7 @@ from velbusaio.messages.module_status import (
82
86
  )
83
87
  from velbusaio.messages.module_status_request import ModuleStatusRequestMessage
84
88
  from velbusaio.messages.push_button_status import PushButtonStatusMessage
89
+ from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
85
90
  from velbusaio.messages.read_data_from_memory import ReadDataFromMemoryMessage
86
91
  from velbusaio.messages.relay_status import RelayStatusMessage, RelayStatusMessage2
87
92
  from velbusaio.messages.sensor_temperature import SensorTemperatureMessage
@@ -90,7 +95,6 @@ from velbusaio.messages.slider_status import SliderStatusMessage
90
95
  from velbusaio.messages.slow_blinking_led import SlowBlinkingLedMessage
91
96
  from velbusaio.messages.temp_sensor_status import TempSensorStatusMessage
92
97
  from velbusaio.messages.update_led_status import UpdateLedStatusMessage
93
- from velbusaio.channels import Temperature as TemperatureChannelType
94
98
 
95
99
 
96
100
  class Module:
@@ -103,7 +107,6 @@ class Module:
103
107
  cls,
104
108
  module_address: int,
105
109
  module_type: int,
106
- module_data: dict,
107
110
  serial: int | None = None,
108
111
  memorymap: int | None = None,
109
112
  build_year: int | None = None,
@@ -114,7 +117,6 @@ class Module:
114
117
  return VmbDali(
115
118
  module_address,
116
119
  module_type,
117
- module_data,
118
120
  serial,
119
121
  memorymap,
120
122
  build_year,
@@ -125,7 +127,6 @@ class Module:
125
127
  return Module(
126
128
  module_address,
127
129
  module_type,
128
- module_data,
129
130
  serial,
130
131
  memorymap,
131
132
  build_year,
@@ -137,7 +138,6 @@ class Module:
137
138
  self,
138
139
  module_address: int,
139
140
  module_type: int,
140
- module_data: dict,
141
141
  serial: int | None = None,
142
142
  memorymap: int | None = None,
143
143
  build_year: int | None = None,
@@ -145,8 +145,8 @@ class Module:
145
145
  cache_dir: str | None = None,
146
146
  ) -> None:
147
147
  self._address = module_address
148
- self._type = module_type
149
- self._data = module_data
148
+ self._type = int(module_type)
149
+ self._data = {}
150
150
 
151
151
  self._name = {}
152
152
  self._sub_address = {}
@@ -159,8 +159,34 @@ class Module:
159
159
  self._channels = {}
160
160
  self.loaded = False
161
161
 
162
- def initialize(self, writer: Callable[[Message], Awaitable[None]]) -> None:
162
+ def get_initial_timeout(self) -> int:
163
+ return SCAN_MODULEINFO_TIMEOUT_INITIAL
164
+
165
+ async def initialize(self, writer: Callable[[Message], Awaitable[None]]) -> None:
163
166
  self._log = logging.getLogger("velbus-module")
167
+ # load the protocol data
168
+ try:
169
+ if sys.version_info >= (3, 13):
170
+ with importlib.resources.path(
171
+ __name__, f"module_spec/{h2(self._type)}.json"
172
+ ) as fspath:
173
+ async with async_open(fspath) as protocol_file:
174
+ self._data = json.loads(await protocol_file.read())
175
+ else:
176
+ async with async_open(
177
+ str(
178
+ importlib.resources.files(__name__.split(".")[0]).joinpath(
179
+ f"module_spec/{h2(self._type)}.json"
180
+ )
181
+ )
182
+ ) as protocol_file:
183
+ self._data = json.loads(await protocol_file.read())
184
+ self._log.debug(f"Module spec {h2(self._type)} loaded")
185
+ except FileNotFoundError:
186
+ self._log.warning(f"No module spec for {h2(self._type)}")
187
+ self._data = {}
188
+
189
+ # set some params from the velbus controller
164
190
  self._writer = writer
165
191
  for chan in self._channels.values():
166
192
  chan._writer = writer
@@ -185,10 +211,10 @@ class Module:
185
211
  ):
186
212
  del self._channels[i]
187
213
 
188
- def _cache(self) -> None:
214
+ async def _cache(self) -> None:
189
215
  cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.json")
190
- with cfile.open("w") as fl:
191
- json.dump(self.to_cache(), fl, indent=4)
216
+ async with async_open(cfile, "w") as fl:
217
+ await fl.write(json.dumps(self.to_cache(), indent=4))
192
218
 
193
219
  def __getstate__(self) -> dict:
194
220
  d = self.__dict__
@@ -265,6 +291,7 @@ class Module:
265
291
  ),
266
292
  ):
267
293
  self._process_channel_name_message(1, message)
294
+ await self._cache()
268
295
  elif isinstance(
269
296
  message,
270
297
  (
@@ -274,6 +301,7 @@ class Module:
274
301
  ),
275
302
  ):
276
303
  self._process_channel_name_message(2, message)
304
+ await self._cache()
277
305
  elif isinstance(
278
306
  message,
279
307
  (
@@ -283,6 +311,7 @@ class Module:
283
311
  ),
284
312
  ):
285
313
  self._process_channel_name_message(3, message)
314
+ await self._cache()
286
315
  elif isinstance(message, MemoryDataMessage):
287
316
  await self._process_memory_data_message(message)
288
317
  elif isinstance(message, (RelayStatusMessage, RelayStatusMessage2)):
@@ -511,8 +540,6 @@ class Module:
511
540
  message.sensor, {"cur": message.value, "unit": message.unit}
512
541
  )
513
542
 
514
- self._cache()
515
-
516
543
  async def _update_channel(self, channel: int, updates: dict):
517
544
  try:
518
545
  await self._channels[channel].update(updates)
@@ -533,8 +560,8 @@ class Module:
533
560
  # see if we have a cache
534
561
  try:
535
562
  cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.json")
536
- with cfile.open("r") as fl:
537
- cache = json.load(fl)
563
+ async with async_open(cfile, "r") as fl:
564
+ cache = json.loads(await fl.read())
538
565
  except OSError:
539
566
  cache = {}
540
567
  # load default channels
@@ -551,6 +578,10 @@ class Module:
551
578
  if "channels" in cache:
552
579
  for num, chan in cache["channels"].items():
553
580
  self._channels[int(num)]._name = chan["name"]
581
+ if "sub_device" in chan:
582
+ self._channels[int(num)]._sub_device = chan["sub_device"]
583
+ else:
584
+ self._channels[int(num)]._sub_device = False
554
585
  if "Unit" in chan:
555
586
  self._channels[int(num)]._Unit = chan["Unit"]
556
587
  self._channels[int(num)]._is_loaded = True
@@ -640,7 +671,7 @@ class Module:
640
671
  )
641
672
  return int(channel)
642
673
 
643
- def is_loaded(self) -> bool:
674
+ async def is_loaded(self) -> bool:
644
675
  """
645
676
  Check if all name messages have been received
646
677
  """
@@ -658,7 +689,7 @@ class Module:
658
689
  return False
659
690
  # set that we finished the module loading
660
691
  self.loaded = True
661
- self._cache()
692
+ await self._cache()
662
693
  return True
663
694
 
664
695
  async def _request_module_status(self) -> None:
@@ -728,11 +759,20 @@ class Module:
728
759
 
729
760
  for chan, chan_data in self._data["Channels"].items():
730
761
  edit = True
762
+ sub = True
731
763
  if "Editable" not in chan_data or chan_data["Editable"] != "yes":
732
764
  edit = False
765
+ if "Subdevice" not in chan_data or chan_data["Subdevice"] != "yes":
766
+ sub = False
733
767
  cls = getattr(sys.modules[__name__], chan_data["Type"])
734
768
  self._channels[int(chan)] = cls(
735
- self, int(chan), chan_data["Name"], edit, self._writer, self._address
769
+ module=self,
770
+ num=int(chan),
771
+ name=chan_data["Name"],
772
+ nameEditable=edit,
773
+ subDevice=sub,
774
+ writer=self._writer,
775
+ address=self._address,
736
776
  )
737
777
  if chan_data["Type"] == "Temperature":
738
778
  if "Thermostat" in self._data or (
@@ -751,7 +791,6 @@ class VmbDali(Module):
751
791
  self,
752
792
  module_address: int,
753
793
  module_type: int,
754
- module_data: dict,
755
794
  serial: int | None = None,
756
795
  memorymap: int | None = None,
757
796
  build_year: int | None = None,
@@ -761,7 +800,6 @@ class VmbDali(Module):
761
800
  super().__init__(
762
801
  module_address,
763
802
  module_type,
764
- module_data,
765
803
  serial,
766
804
  memorymap,
767
805
  build_year,
@@ -770,10 +808,19 @@ class VmbDali(Module):
770
808
  )
771
809
  self.group_members: dict[int, set[int]] = {}
772
810
 
811
+ def get_initial_timeout(self) -> int:
812
+ return 100000
813
+
773
814
  async def _load_default_channels(self) -> None:
774
815
  for chan in range(1, 64 + 1):
775
816
  self._channels[chan] = Channel(
776
- self, chan, "placeholder", True, self._writer, self._address
817
+ module=self,
818
+ num=chan,
819
+ name="placeholder",
820
+ nameEditable=True,
821
+ subDevice=True,
822
+ writer=self._writer,
823
+ address=self._address,
777
824
  )
778
825
  # Placeholders will keep this module loading
779
826
  # Until the DaliDeviceSettings messages either delete or replace these placeholder's
@@ -804,6 +851,7 @@ class VmbDali(Module):
804
851
  message.channel,
805
852
  None,
806
853
  True,
854
+ True,
807
855
  self._writer,
808
856
  self._address,
809
857
  slider_scale=254,
@@ -853,7 +901,7 @@ class VmbDali(Module):
853
901
  else:
854
902
  return await super().on_message(message)
855
903
 
856
- self._cache()
904
+ await self._cache()
857
905
 
858
906
  async def _request_channel_name(self) -> None:
859
907
  # Channel names are requested after channel scan