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
@@ -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
  import struct
velbusaio/module.py CHANGED
@@ -1,16 +1,21 @@
1
1
  """
2
2
  This represents a velbus module
3
3
  """
4
+
4
5
  from __future__ import annotations
5
6
 
7
+ import asyncio
8
+ import importlib.resources
9
+ import json
6
10
  import logging
7
11
  import os
8
12
  import pathlib
9
- import pickle
10
13
  import struct
11
14
  import sys
12
15
  from typing import Awaitable, Callable
13
16
 
17
+ from aiofile import async_open
18
+
14
19
  from velbusaio.channels import (
15
20
  Blind,
16
21
  Button,
@@ -24,7 +29,10 @@ from velbusaio.channels import (
24
29
  SelectedProgram,
25
30
  Sensor,
26
31
  SensorNumber,
27
- Temperature,
32
+ )
33
+ from velbusaio.channels import Temperature
34
+ from velbusaio.channels import Temperature as TemperatureChannelType
35
+ from velbusaio.channels import (
28
36
  ThermostatChannel,
29
37
  )
30
38
  from velbusaio.command_registry import commandRegistry
@@ -33,10 +41,10 @@ from velbusaio.const import (
33
41
  CHANNEL_MEMO_TEXT,
34
42
  CHANNEL_SELECTED_PROGRAM,
35
43
  PRIORITY_LOW,
44
+ SCAN_MODULEINFO_TIMEOUT_INITIAL,
36
45
  )
37
- from velbusaio.helpers import handle_match, keys_exists
46
+ from velbusaio.helpers import h2, handle_match, keys_exists
38
47
  from velbusaio.message import Message
39
- from velbusaio.messages import DaliDeviceSettingMsg
40
48
  from velbusaio.messages.blind_status import BlindStatusMessage, BlindStatusNgMessage
41
49
  from velbusaio.messages.channel_name_part1 import (
42
50
  ChannelNamePart1Message,
@@ -56,23 +64,28 @@ from velbusaio.messages.channel_name_part3 import (
56
64
  from velbusaio.messages.channel_name_request import (
57
65
  COMMAND_CODE as CHANNEL_NAME_REQUEST_COMMAND_CODE,
58
66
  )
59
- from velbusaio.messages.channel_name_request import ChannelNameRequestMessage
67
+ from velbusaio.messages.channel_name_request import (
68
+ ChannelNameRequestMessage,
69
+ )
60
70
  from velbusaio.messages.clear_led import ClearLedMessage
61
71
  from velbusaio.messages.counter_status import CounterStatusMessage
62
72
  from velbusaio.messages.counter_status_request import CounterStatusRequestMessage
73
+ from velbusaio.messages.counter_value import CounterValueMessage
74
+ from velbusaio.messages.dali_device_settings import DaliDeviceSettingMsg
63
75
  from velbusaio.messages.dali_device_settings import DeviceType as DaliDeviceType
64
76
  from velbusaio.messages.dali_device_settings import DeviceTypeMsg as DaliDeviceTypeMsg
65
77
  from velbusaio.messages.dali_device_settings import MemberOfGroupMsg
66
78
  from velbusaio.messages.dali_device_settings_request import (
67
79
  COMMAND_CODE as DALI_DEVICE_SETTINGS_REQUEST_COMMAND_CODE,
68
80
  )
69
- from velbusaio.messages.dali_device_settings_request import DaliDeviceSettingsRequest
81
+ from velbusaio.messages.dali_device_settings_request import (
82
+ DaliDeviceSettingsRequest,
83
+ )
70
84
  from velbusaio.messages.dali_dim_value_status import DimValueStatus
71
85
  from velbusaio.messages.dimmer_channel_status import DimmerChannelStatusMessage
72
86
  from velbusaio.messages.dimmer_status import DimmerStatusMessage
73
87
  from velbusaio.messages.fast_blinking_led import FastBlinkingLedMessage
74
88
  from velbusaio.messages.memory_data import MemoryDataMessage
75
- from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
76
89
  from velbusaio.messages.module_status import (
77
90
  ModuleStatusGP4PirMessage,
78
91
  ModuleStatusMessage,
@@ -80,9 +93,8 @@ from velbusaio.messages.module_status import (
80
93
  ModuleStatusPirMessage,
81
94
  )
82
95
  from velbusaio.messages.module_status_request import ModuleStatusRequestMessage
83
- from velbusaio.messages.module_subtype import ModuleSubTypeMessage
84
- from velbusaio.messages.module_type import ModuleTypeMessage, ModuleType2Message
85
96
  from velbusaio.messages.push_button_status import PushButtonStatusMessage
97
+ from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
86
98
  from velbusaio.messages.read_data_from_memory import ReadDataFromMemoryMessage
87
99
  from velbusaio.messages.relay_status import RelayStatusMessage, RelayStatusMessage2
88
100
  from velbusaio.messages.sensor_temperature import SensorTemperatureMessage
@@ -91,7 +103,6 @@ from velbusaio.messages.slider_status import SliderStatusMessage
91
103
  from velbusaio.messages.slow_blinking_led import SlowBlinkingLedMessage
92
104
  from velbusaio.messages.temp_sensor_status import TempSensorStatusMessage
93
105
  from velbusaio.messages.update_led_status import UpdateLedStatusMessage
94
- from velbusaio.channels import Temperature as TemperatureChannelType
95
106
 
96
107
 
97
108
  class Module:
@@ -104,18 +115,16 @@ class Module:
104
115
  cls,
105
116
  module_address: int,
106
117
  module_type: int,
107
- module_data: dict,
108
118
  serial: int | None = None,
109
119
  memorymap: int | None = None,
110
120
  build_year: int | None = None,
111
121
  build_week: int | None = None,
112
122
  cache_dir: str | None = None,
113
123
  ) -> Module:
114
- if module_type == 0x45:
124
+ if module_type == 0x45 or module_type == 0x5A:
115
125
  return VmbDali(
116
126
  module_address,
117
127
  module_type,
118
- module_data,
119
128
  serial,
120
129
  memorymap,
121
130
  build_year,
@@ -126,7 +135,6 @@ class Module:
126
135
  return Module(
127
136
  module_address,
128
137
  module_type,
129
- module_data,
130
138
  serial,
131
139
  memorymap,
132
140
  build_year,
@@ -138,7 +146,6 @@ class Module:
138
146
  self,
139
147
  module_address: int,
140
148
  module_type: int,
141
- module_data: dict,
142
149
  serial: int | None = None,
143
150
  memorymap: int | None = None,
144
151
  build_year: int | None = None,
@@ -146,8 +153,8 @@ class Module:
146
153
  cache_dir: str | None = None,
147
154
  ) -> None:
148
155
  self._address = module_address
149
- self._type = module_type
150
- self._data = module_data
156
+ self._type = int(module_type)
157
+ self._data = {}
151
158
 
152
159
  self._name = {}
153
160
  self._sub_address = {}
@@ -157,11 +164,47 @@ class Module:
157
164
  self.build_week = build_week
158
165
  self._cache_dir = cache_dir
159
166
  self._is_loading = False
167
+ self._got_status = asyncio.Event()
168
+ self._got_status.clear()
160
169
  self._channels = {}
161
170
  self.loaded = False
171
+ self._use_cache = True
172
+ self._loaded_cache = {}
173
+
174
+ async def wait_for_status_messages(self) -> None:
175
+ try:
176
+ await asyncio.wait_for(self._got_status.wait(), 2)
177
+ except Exception:
178
+ self._log.warning(f"Timeout waiting for status messages for: {self}")
162
179
 
163
- def initialize(self, writer: Callable[[Message], Awaitable[None]]) -> None:
180
+ def get_initial_timeout(self) -> int:
181
+ return SCAN_MODULEINFO_TIMEOUT_INITIAL
182
+
183
+ async def initialize(self, writer: Callable[[Message], Awaitable[None]]) -> None:
164
184
  self._log = logging.getLogger("velbus-module")
185
+ # load the protocol data
186
+ try:
187
+ if sys.version_info >= (3, 13):
188
+ with importlib.resources.path(
189
+ __name__, f"module_spec/{h2(self._type)}.json"
190
+ ) as fspath:
191
+ async with async_open(fspath) as protocol_file:
192
+ self._data = json.loads(await protocol_file.read())
193
+ else:
194
+ async with async_open(
195
+ str(
196
+ importlib.resources.files(__name__.split(".")[0]).joinpath(
197
+ f"module_spec/{h2(self._type)}.json"
198
+ )
199
+ )
200
+ ) as protocol_file:
201
+ self._data = json.loads(await protocol_file.read())
202
+ self._log.debug(f"Module spec {h2(self._type)} loaded")
203
+ except FileNotFoundError:
204
+ self._log.warning(f"No module spec for {h2(self._type)}")
205
+ self._data = {}
206
+
207
+ # set some params from the velbus controller
165
208
  self._writer = writer
166
209
  for chan in self._channels.values():
167
210
  chan._writer = writer
@@ -186,10 +229,12 @@ class Module:
186
229
  ):
187
230
  del self._channels[i]
188
231
 
189
- def _cache(self) -> None:
190
- cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.p")
191
- with cfile.open("wb") as fl:
192
- pickle.dump(self, fl)
232
+ async def _cache(self) -> None:
233
+ if not self._use_cache:
234
+ return
235
+ cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.json")
236
+ async with async_open(cfile, "w") as fl:
237
+ await fl.write(json.dumps(self.to_cache(), indent=4))
193
238
 
194
239
  def __getstate__(self) -> dict:
195
240
  d = self.__dict__
@@ -200,20 +245,20 @@ class Module:
200
245
  self.__dict__ = state
201
246
 
202
247
  def __repr__(self) -> str:
203
- return (
204
- "<{}: {{{}}} @ {{{}}} loaded:{{{}}} loading:{{{}}} channels{{:{}}}>".format(
205
- self._name,
206
- self._type,
207
- self._address,
208
- self.loaded,
209
- self._is_loading,
210
- self._channels,
211
- )
212
- )
248
+ return f"<{self._name} type:{self._type} address:{self._address} loaded:{self.loaded} loading:{self._is_loading} channels: {self._channels}>"
213
249
 
214
250
  def __str__(self) -> str:
215
251
  return self.__repr__()
216
252
 
253
+ def to_cache(self) -> dict:
254
+ d = {"name": self._name, "channels": {}}
255
+ for num, chan in self._channels.items():
256
+ d["channels"][num] = chan.to_cache()
257
+ return d
258
+
259
+ def get_address(self) -> int:
260
+ return self._address
261
+
217
262
  def get_addresses(self) -> list:
218
263
  """
219
264
  Get all addresses for this module
@@ -231,7 +276,9 @@ class Module:
231
276
  return self._type
232
277
 
233
278
  def get_type_name(self) -> str:
234
- return self._data["Type"]
279
+ if "Type" in self._data:
280
+ return self._data["Type"]
281
+ return "UNKNOWN"
235
282
 
236
283
  def get_serial(self) -> str | None:
237
284
  return self.serial
@@ -240,12 +287,7 @@ class Module:
240
287
  return self._name
241
288
 
242
289
  def get_sw_version(self) -> str:
243
- return "{}-{}.{}.{}".format(
244
- self.serial,
245
- self.memory_map_version,
246
- self.build_year,
247
- self.build_week,
248
- )
290
+ return f"{self.serial}-{self.memory_map_version}.{self.build_year}.{self.build_week}"
249
291
 
250
292
  def calc_channel_offset(self, address: int) -> int:
251
293
  _channel_offset = 0
@@ -260,6 +302,7 @@ class Module:
260
302
  """
261
303
  Process received message
262
304
  """
305
+ self._log.debug(f"RX: {message}")
263
306
  _channel_offset = self.calc_channel_offset(message.address)
264
307
 
265
308
  if isinstance(
@@ -271,6 +314,7 @@ class Module:
271
314
  ),
272
315
  ):
273
316
  self._process_channel_name_message(1, message)
317
+ await self._cache()
274
318
  elif isinstance(
275
319
  message,
276
320
  (
@@ -280,6 +324,7 @@ class Module:
280
324
  ),
281
325
  ):
282
326
  self._process_channel_name_message(2, message)
327
+ await self._cache()
283
328
  elif isinstance(
284
329
  message,
285
330
  (
@@ -289,6 +334,7 @@ class Module:
289
334
  ),
290
335
  ):
291
336
  self._process_channel_name_message(3, message)
337
+ await self._cache()
292
338
  elif isinstance(message, MemoryDataMessage):
293
339
  await self._process_memory_data_message(message)
294
340
  elif isinstance(message, (RelayStatusMessage, RelayStatusMessage2)):
@@ -516,14 +562,22 @@ class Module:
516
562
  await self._update_channel(
517
563
  message.sensor, {"cur": message.value, "unit": message.unit}
518
564
  )
519
-
520
- self._cache()
565
+ elif isinstance(message, CounterValueMessage):
566
+ await self._update_channel(
567
+ message.channel, {"power": message.power, "energy": message.energy}
568
+ )
569
+ elif isinstance(message, DimValueStatus):
570
+ for offset, dim_value in enumerate(message.dim_values):
571
+ channel = message.channel + offset
572
+ await self._update_channel(channel, {"state": dim_value})
573
+ # notigy status
574
+ self._got_status.set()
521
575
 
522
576
  async def _update_channel(self, channel: int, updates: dict):
523
577
  try:
524
578
  await self._channels[channel].update(updates)
525
579
  except KeyError:
526
- self._log.error(
580
+ self._log.info(
527
581
  f"channel {channel} does not exist for module @ address {self}"
528
582
  )
529
583
 
@@ -533,33 +587,61 @@ class Module:
533
587
  """
534
588
  return self._channels
535
589
 
590
+ async def load_from_vlp(self, vlp_data: dict) -> None:
591
+ self._name = vlp_data.get_name()
592
+ self._data["Channels"] = vlp_data.get_channels()
593
+ self._use_cache = False
594
+ self._is_loading = False
595
+ self.loaded = True
596
+ await self._load_default_channels()
597
+ # TODO set all channels to _is_loaded = True
598
+ for chan in self._channels.values():
599
+ chan._is_loaded = True
600
+ await self._request_module_status()
601
+
536
602
  async def load(self, from_cache: bool = False) -> None:
537
- """
538
- Retrieve names of channels
539
- """
540
- # did we already start the loading?
541
- # this is needed for the submodules,
542
- # as the submodule address maps to the main module
543
- # this method can be called multiple times
544
- if self._is_loading or self.loaded:
545
- if from_cache:
546
- await self._request_module_status()
547
- return
548
- self._log.info("Load Module")
549
603
  # start the loading
550
604
  self._is_loading = True
605
+ # see if we have a cache
606
+ cache = await self._get_cache()
607
+ self._loaded_cache = cache
551
608
  # load default channels
552
- await self.__load_default_channels()
609
+ await self._load_default_channels()
610
+
553
611
  # load the data from memory ( the stuff that we need)
554
- await self.__load_memory()
612
+ if "name" in cache and cache["name"] != "":
613
+ self._name = cache["name"]
614
+ else:
615
+ await self.__load_memory()
555
616
  # load the module status
556
- await self._request_module_status()
617
+ # await self._request_module_status()
557
618
  # load the channel names
558
- await self._request_channel_name()
619
+ if "channels" in cache:
620
+ for num, chan in cache["channels"].items():
621
+ self._channels[int(num)]._name = chan["name"]
622
+ if "subdevice" in chan:
623
+ self._channels[int(num)]._sub_device = chan["subdevice"]
624
+ else:
625
+ self._channels[int(num)]._sub_device = False
626
+ if "Unit" in chan:
627
+ self._channels[int(num)]._Unit = chan["Unit"]
628
+ self._channels[int(num)]._is_loaded = True
629
+ else:
630
+ await self._request_channel_name()
559
631
  # load the module specific stuff
560
632
  self._load()
561
633
  # stop the loading
562
634
  self._is_loading = False
635
+ await self._request_module_status()
636
+
637
+ async def _get_cache(self):
638
+ try:
639
+ cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.json")
640
+ async with async_open(cfile, "r") as fl:
641
+ cache = json.loads(await fl.read())
642
+ except OSError:
643
+ cache = {}
644
+ return cache
563
645
 
564
646
  def _load(self) -> None:
565
647
  """
@@ -586,7 +668,11 @@ class Module:
586
668
  addr = "{high:02X}{low:02X}".format(
587
669
  high=message.high_address, low=message.low_address
588
670
  )
589
- mdata = self._data["Memory"]["1"]["Address"][addr]
671
+ if "Memory" not in self._data:
672
+ return
673
+ if "Address" not in self._data["Memory"]:
674
+ return
675
+ mdata = self._data["Memory"]["Address"][addr]
590
676
  if "ModuleName" in mdata and isinstance(self._name, dict):
591
677
  # if self._name is a dict we are still loading
592
678
  # if its a string it was already complete
@@ -635,7 +721,7 @@ class Module:
635
721
  )
636
722
  return int(channel)
637
723
 
638
- def is_loaded(self) -> bool:
724
+ async def is_loaded(self) -> bool:
639
725
  """
640
726
  Check if all name messages have been received
641
727
  """
@@ -653,7 +739,7 @@ class Module:
653
739
  return False
654
740
  # set that we finished the module loading
655
741
  self.loaded = True
656
- self._cache()
742
+ await self._cache()
657
743
  return True
658
744
 
659
745
  async def _request_module_status(self) -> None:
@@ -661,15 +747,20 @@ class Module:
661
747
  if "Channels" not in self._data:
662
748
  # some modules have no channels
663
749
  return
750
+ self._log.info(f"Request module status {self._address}")
751
+
664
752
  mod_stat_req_msg = ModuleStatusRequestMessage(self._address)
665
753
  counter_msg = None
666
- for chan, chan_data in self._data["Channels"].items():
667
- if int(chan) < 9 and chan_data["Type"] in ("Blind", "Dimmer", "Relay"):
668
- mod_stat_req_msg.channels.append(int(chan))
669
- if chan_data["Type"] == "ButtonCounter":
670
- if counter_msg is None:
671
- counter_msg = CounterStatusRequestMessage(self._address)
672
- counter_msg.channels.append(int(chan))
754
+ if keys_exists(self._data, "AllChannelStatus"):
755
+ mod_stat_req_msg.channels = self._data["AllChannelStatus"]
756
+ else:
757
+ for chan, chan_data in self._data["Channels"].items():
758
+ if int(chan) < 9 and chan_data["Type"] in ("Blind", "Dimmer", "Relay"):
759
+ mod_stat_req_msg.channels.append(int(chan))
760
+ if chan_data["Type"] == "ButtonCounter":
761
+ if counter_msg is None:
762
+ counter_msg = CounterStatusRequestMessage(self._address)
763
+ counter_msg.channels.append(int(chan))
673
764
  await self._writer(mod_stat_req_msg)
674
765
  if counter_msg is not None:
675
766
  await self._writer(counter_msg)
@@ -702,9 +793,10 @@ class Module:
702
793
  self._name = None
703
794
  return
704
795
 
705
- for _memory_key, memory_part in self._data["Memory"].items():
706
- if "Address" in memory_part:
707
- for addr_int in memory_part["Address"].keys():
796
+ for memory_key, memory_part in self._data["Memory"].items():
797
+
798
+ if memory_key == "Address":
799
+ for addr_int in memory_part.keys():
708
800
  addr = struct.unpack(
709
801
  ">BB", struct.pack(">h", int("0x" + addr_int, 0))
710
802
  )
@@ -714,34 +806,34 @@ class Module:
714
806
  msg.low_address = addr[1]
715
807
  await self._writer(msg)
716
808
 
717
- async def __load_default_channels(self) -> None:
809
+ async def _load_default_channels(self) -> None:
718
810
  if "Channels" not in self._data:
719
811
  return
720
812
 
721
813
  for chan, chan_data in self._data["Channels"].items():
722
814
  edit = True
815
+ sub = True
723
816
  if "Editable" not in chan_data or chan_data["Editable"] != "yes":
724
817
  edit = False
818
+ if "Subdevice" not in chan_data or chan_data["Subdevice"] != "yes":
819
+ sub = False
725
820
  cls = getattr(sys.modules[__name__], chan_data["Type"])
726
821
  self._channels[int(chan)] = cls(
727
- self, int(chan), chan_data["Name"], edit, self._writer, self._address
822
+ module=self,
823
+ num=int(chan),
824
+ name=chan_data["Name"],
825
+ nameEditable=edit,
826
+ subDevice=sub,
827
+ writer=self._writer,
828
+ address=self._address,
728
829
  )
729
830
  if chan_data["Type"] == "Temperature":
730
831
  if "Thermostat" in self._data or (
731
832
  "ThermostatAddr" in self._data and self._data["ThermostatAddr"] != 0
732
833
  ):
733
834
  await self._update_channel(int(chan), {"thermostat": True})
734
- # add extra channel for program selection which is not in the channel list of the protocol.json file,
735
- # but is available in the messages list of the corresponding module.
736
- if keys_exists(self._data, "Messages", "B3"):
737
- self._channels[CHANNEL_SELECTED_PROGRAM] = SelectedProgram(
738
- self,
739
- CHANNEL_SELECTED_PROGRAM,
740
- "Selected Program",
741
- False,
742
- self._writer,
743
- self._address,
744
- )
835
+ if chan_data["Type"] == "Dimmer" and "sliderScale" in self._data:
836
+ self._channels[int(chan)].slider_scale = self._data["sliderScale"]
745
837
 
746
838
 
747
839
  class VmbDali(Module):
@@ -754,7 +846,6 @@ class VmbDali(Module):
754
846
  self,
755
847
  module_address: int,
756
848
  module_type: int,
757
- module_data: dict,
758
849
  serial: int | None = None,
759
850
  memorymap: int | None = None,
760
851
  build_year: int | None = None,
@@ -764,7 +855,6 @@ class VmbDali(Module):
764
855
  super().__init__(
765
856
  module_address,
766
857
  module_type,
767
- module_data,
768
858
  serial,
769
859
  memorymap,
770
860
  build_year,
@@ -773,19 +863,24 @@ class VmbDali(Module):
773
863
  )
774
864
  self.group_members: dict[int, set[int]] = {}
775
865
 
776
- async def load(self, from_cache: bool = False) -> None:
777
- await super().load(from_cache)
778
-
779
- if not from_cache:
780
- for chan in range(1, 64 + 1):
781
- self._channels[chan] = Channel(
782
- self, chan, "placeholder", True, self._writer, self._address
783
- )
784
- # Placeholders will keep this module loading
785
- # Until the DaliDeviceSettings messages either delete or replace these placeholder's
786
- # with actual channels
787
-
788
- await self._request_dali_channels()
866
+ def get_initial_timeout(self) -> int:
867
+ return 100000
868
+
869
+ async def _load_default_channels(self) -> None:
870
+ for chan in range(1, 64 + 1):
871
+ self._channels[chan] = Channel(
872
+ module=self,
873
+ num=chan,
874
+ name="placeholder",
875
+ nameEditable=True,
876
+ subDevice=True,
877
+ writer=self._writer,
878
+ address=self._address,
879
+ )
880
+ # Placeholders will keep this module loading
881
+ # Until the DaliDeviceSettings messages either delete or replace these placeholder's
882
+ # with actual channels
883
+ await self._request_dali_channels()
789
884
 
790
885
  async def _request_dali_channels(self):
791
886
  msg_type = commandRegistry.get_command(
@@ -804,13 +899,32 @@ class VmbDali(Module):
804
899
  if message.channel in self._channels:
805
900
  del self._channels[message.channel]
806
901
  elif message.data.device_type == DaliDeviceType.LedModule:
807
- if self._channels.get(message.channel).__class__ != Dimmer:
902
+ cache = self._loaded_cache
903
+ if (
904
+ "channels" in cache
905
+ and str(message.channel) in cache["channels"]
906
+ and cache["channels"][str(message.channel)]["type"] == "Dimmer"
907
+ ):
908
+ # If we have a cached dimmer channel, use that name
909
+ name = cache["channels"][str(message.channel)]["name"]
910
+ self._channels[message.channel] = Dimmer(
911
+ self,
912
+ message.channel,
913
+ name,
914
+ False, # set False to enable an already loaded Dimmer
915
+ True,
916
+ self._writer,
917
+ self._address,
918
+ slider_scale=254,
919
+ )
920
+ elif self._channels.get(message.channel).__class__ != Dimmer:
808
921
  # New or changed type, replace channel:
809
922
  self._channels[message.channel] = Dimmer(
810
923
  self,
811
924
  message.channel,
812
925
  None,
813
926
  True,
927
+ True,
814
928
  self._writer,
815
929
  self._address,
816
930
  slider_scale=254,
@@ -860,8 +974,6 @@ class VmbDali(Module):
860
974
  else:
861
975
  return await super().on_message(message)
862
976
 
863
- self._cache()
864
-
865
977
  async def _request_channel_name(self) -> None:
866
978
  # Channel names are requested after channel scan
867
979
  # don't do them here (at initialization time)
@@ -0,0 +1,62 @@
1
+ {
2
+ "Channels": {
3
+ "01": {
4
+ "Editable": "yes",
5
+ "Name": "Push button 1",
6
+ "Type": "Button"
7
+ },
8
+ "02": {
9
+ "Editable": "yes",
10
+ "Name": "Push button 2",
11
+ "Type": "Button"
12
+ },
13
+ "03": {
14
+ "Editable": "yes",
15
+ "Name": "Push button 3",
16
+ "Type": "Button"
17
+ },
18
+ "04": {
19
+ "Editable": "yes",
20
+ "Name": "Push button 4",
21
+ "Type": "Button"
22
+ },
23
+ "05": {
24
+ "Editable": "yes",
25
+ "Name": "Push button 5",
26
+ "Type": "Button"
27
+ },
28
+ "06": {
29
+ "Editable": "yes",
30
+ "Name": "Push button 6",
31
+ "Type": "Button"
32
+ },
33
+ "07": {
34
+ "Editable": "yes",
35
+ "Name": "Push button 7",
36
+ "Type": "Button"
37
+ },
38
+ "08": {
39
+ "Editable": "yes",
40
+ "Name": "Push button 8",
41
+ "Type": "Button"
42
+ },
43
+ "96": {
44
+ "Name": "SelectedProgram",
45
+ "Type": "SelectedProgram"
46
+ }
47
+ },
48
+ "Info": "8-Channel Push Button Module",
49
+ "Type": "VMB8PB",
50
+ "Memory": {
51
+ "Channels": {
52
+ "01": "0000-000E",
53
+ "02": "0010-001E",
54
+ "03": "0020-002E",
55
+ "04": "0030-003E",
56
+ "05": "0040-004E",
57
+ "06": "0050-005E",
58
+ "07": "0060-006E",
59
+ "08": "0070-007E"
60
+ }
61
+ }
62
+ }