velbus-aio 2021.8.7__py3-none-any.whl → 2025.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. scripts/parse_specs.py +156 -0
  2. velbus_aio-2025.11.0.dist-info/METADATA +71 -0
  3. velbus_aio-2025.11.0.dist-info/RECORD +194 -0
  4. {velbus_aio-2021.8.7.dist-info → velbus_aio-2025.11.0.dist-info}/WHEEL +1 -1
  5. velbus_aio-2025.11.0.dist-info/top_level.txt +3 -0
  6. velbusaio/channels.py +443 -109
  7. velbusaio/command_registry.py +126 -13
  8. velbusaio/const.py +36 -12
  9. velbusaio/controller.py +252 -177
  10. velbusaio/discovery.py +2 -2
  11. velbusaio/exceptions.py +22 -0
  12. velbusaio/handler.py +311 -145
  13. velbusaio/helpers.py +6 -18
  14. velbusaio/message.py +46 -132
  15. velbusaio/messages/__init__.py +12 -2
  16. velbusaio/messages/blind_status.py +16 -25
  17. velbusaio/messages/bus_active.py +3 -9
  18. velbusaio/messages/bus_error_counter_status.py +3 -4
  19. velbusaio/messages/bus_error_counter_status_request.py +3 -4
  20. velbusaio/messages/bus_off.py +3 -4
  21. velbusaio/messages/channel_name_part1.py +49 -33
  22. velbusaio/messages/channel_name_part2.py +49 -33
  23. velbusaio/messages/channel_name_part3.py +49 -33
  24. velbusaio/messages/channel_name_request.py +26 -12
  25. velbusaio/messages/clear_led.py +3 -4
  26. velbusaio/messages/counter_status.py +3 -17
  27. velbusaio/messages/counter_status_request.py +6 -6
  28. velbusaio/messages/counter_value.py +44 -0
  29. velbusaio/messages/cover_down.py +4 -29
  30. velbusaio/messages/cover_off.py +5 -29
  31. velbusaio/messages/cover_position.py +4 -19
  32. velbusaio/messages/cover_up.py +4 -27
  33. velbusaio/messages/dali_device_settings.py +178 -0
  34. velbusaio/messages/dali_device_settings_request.py +53 -0
  35. velbusaio/messages/dali_dim_value_status.py +44 -0
  36. velbusaio/messages/dimmer_channel_status.py +6 -19
  37. velbusaio/messages/dimmer_status.py +14 -31
  38. velbusaio/messages/edge_set_color.py +114 -0
  39. velbusaio/messages/edge_set_custom_color.py +56 -0
  40. velbusaio/messages/fast_blinking_led.py +3 -4
  41. velbusaio/messages/forced_off.py +3 -4
  42. velbusaio/messages/forced_on.py +3 -4
  43. velbusaio/messages/interface_status_request.py +3 -4
  44. velbusaio/messages/ir_receiver_status.py +18 -0
  45. velbusaio/messages/kwh_status.py +3 -19
  46. velbusaio/messages/light_value_request.py +3 -4
  47. velbusaio/messages/memo_text.py +3 -5
  48. velbusaio/messages/memory_data.py +3 -16
  49. velbusaio/messages/memory_data_block.py +3 -4
  50. velbusaio/messages/memory_dump_request.py +3 -4
  51. velbusaio/messages/module_status.py +107 -55
  52. velbusaio/messages/module_status_request.py +7 -6
  53. velbusaio/messages/module_subtype.py +11 -19
  54. velbusaio/messages/module_type.py +132 -21
  55. velbusaio/messages/module_type_request.py +1 -0
  56. velbusaio/messages/psu_load.py +56 -0
  57. velbusaio/messages/psu_values.py +53 -0
  58. velbusaio/messages/push_button_status.py +3 -16
  59. velbusaio/messages/raw.py +74 -0
  60. velbusaio/messages/read_data_block_from_memory.py +3 -4
  61. velbusaio/messages/read_data_from_memory.py +3 -4
  62. velbusaio/messages/realtime_clock_status_request.py +3 -4
  63. velbusaio/messages/receive_buffer_full.py +3 -4
  64. velbusaio/messages/receive_ready.py +3 -4
  65. velbusaio/messages/relay_status.py +13 -42
  66. velbusaio/messages/restore_dimmer.py +33 -24
  67. velbusaio/messages/select_program.py +35 -0
  68. velbusaio/messages/sensor_settings_request.py +3 -4
  69. velbusaio/messages/sensor_temp_request.py +3 -4
  70. velbusaio/messages/sensor_temperature.py +15 -19
  71. velbusaio/messages/set_date.py +10 -30
  72. velbusaio/messages/set_daylight_saving.py +8 -24
  73. velbusaio/messages/set_dimmer.py +43 -41
  74. velbusaio/messages/set_led.py +3 -4
  75. velbusaio/messages/set_realtime_clock.py +10 -30
  76. velbusaio/messages/set_temperature.py +3 -4
  77. velbusaio/messages/slider_status.py +16 -20
  78. velbusaio/messages/slow_blinking_led.py +3 -4
  79. velbusaio/messages/start_relay_blinking_timer.py +3 -4
  80. velbusaio/messages/start_relay_timer.py +3 -4
  81. velbusaio/messages/switch_relay_off.py +3 -16
  82. velbusaio/messages/switch_relay_on.py +3 -16
  83. velbusaio/messages/switch_to_comfort.py +4 -15
  84. velbusaio/messages/switch_to_day.py +4 -15
  85. velbusaio/messages/switch_to_night.py +4 -15
  86. velbusaio/messages/switch_to_safe.py +4 -15
  87. velbusaio/messages/temp_sensor_settings_part1.py +3 -4
  88. velbusaio/messages/temp_sensor_settings_part2.py +27 -0
  89. velbusaio/messages/temp_sensor_settings_part3.py +27 -0
  90. velbusaio/messages/temp_sensor_settings_part4.py +27 -0
  91. velbusaio/messages/temp_sensor_settings_request.py +3 -4
  92. velbusaio/messages/temp_sensor_status.py +34 -35
  93. velbusaio/messages/temp_set_cooling.py +3 -13
  94. velbusaio/messages/temp_set_heating.py +3 -13
  95. velbusaio/messages/update_led_status.py +3 -4
  96. velbusaio/messages/very_fast_blinking_led.py +3 -4
  97. velbusaio/messages/write_data_to_memory.py +3 -4
  98. velbusaio/messages/write_memory_block.py +3 -4
  99. velbusaio/messages/write_module_address_and_serial_number.py +3 -4
  100. velbusaio/module.py +680 -158
  101. velbusaio/module_spec/01.json +62 -0
  102. velbusaio/module_spec/02.json +16 -0
  103. velbusaio/module_spec/03.json +23 -0
  104. velbusaio/module_spec/04.json +283 -0
  105. velbusaio/module_spec/05.json +54 -0
  106. velbusaio/module_spec/06.json +110 -0
  107. velbusaio/module_spec/07.json +16 -0
  108. velbusaio/module_spec/08.json +38 -0
  109. velbusaio/module_spec/09.json +30 -0
  110. velbusaio/module_spec/0A.json +58 -0
  111. velbusaio/module_spec/0B.json +58 -0
  112. velbusaio/module_spec/0C.json +18 -0
  113. velbusaio/module_spec/0E.json +25 -0
  114. velbusaio/module_spec/0F.json +16 -0
  115. velbusaio/module_spec/10.json +111 -0
  116. velbusaio/module_spec/11.json +111 -0
  117. velbusaio/module_spec/12.json +73 -0
  118. velbusaio/module_spec/13.json +4 -0
  119. velbusaio/module_spec/14.json +16 -0
  120. velbusaio/module_spec/15.json +83 -0
  121. velbusaio/module_spec/16.json +129 -0
  122. velbusaio/module_spec/17.json +129 -0
  123. velbusaio/module_spec/18.json +129 -0
  124. velbusaio/module_spec/1A.json +79 -0
  125. velbusaio/module_spec/1B.json +107 -0
  126. velbusaio/module_spec/1D.json +89 -0
  127. velbusaio/module_spec/1E.json +306 -0
  128. velbusaio/module_spec/1F.json +178 -0
  129. velbusaio/module_spec/20.json +178 -0
  130. velbusaio/module_spec/21.json +326 -0
  131. velbusaio/module_spec/22.json +426 -0
  132. velbusaio/module_spec/23.json +129 -0
  133. velbusaio/module_spec/24.json +30 -0
  134. velbusaio/module_spec/25.json +3 -0
  135. velbusaio/module_spec/28.json +454 -0
  136. velbusaio/module_spec/29.json +235 -0
  137. velbusaio/module_spec/2A.json +239 -0
  138. velbusaio/module_spec/2B.json +239 -0
  139. velbusaio/module_spec/2C.json +257 -0
  140. velbusaio/module_spec/2D.json +270 -0
  141. velbusaio/module_spec/2E.json +215 -0
  142. velbusaio/module_spec/2F.json +211 -0
  143. velbusaio/module_spec/30.json +58 -0
  144. velbusaio/module_spec/31.json +465 -0
  145. velbusaio/module_spec/32.json +385 -0
  146. velbusaio/module_spec/33.json +249 -0
  147. velbusaio/module_spec/34.json +313 -0
  148. velbusaio/module_spec/35.json +313 -0
  149. velbusaio/module_spec/36.json +313 -0
  150. velbusaio/module_spec/37.json +333 -0
  151. velbusaio/module_spec/38.json +111 -0
  152. velbusaio/module_spec/39.json +4 -0
  153. velbusaio/module_spec/3A.json +306 -0
  154. velbusaio/module_spec/3B.json +306 -0
  155. velbusaio/module_spec/3C.json +306 -0
  156. velbusaio/module_spec/3D.json +454 -0
  157. velbusaio/module_spec/3E.json +302 -0
  158. velbusaio/module_spec/3F.json +4 -0
  159. velbusaio/module_spec/40.json +4 -0
  160. velbusaio/module_spec/41.json +241 -0
  161. velbusaio/module_spec/42.json +4 -0
  162. velbusaio/module_spec/43.json +23 -0
  163. velbusaio/module_spec/44.json +38 -0
  164. velbusaio/module_spec/45.json +4 -0
  165. velbusaio/module_spec/48.json +111 -0
  166. velbusaio/module_spec/49.json +111 -0
  167. velbusaio/module_spec/4A.json +89 -0
  168. velbusaio/module_spec/4B.json +138 -0
  169. velbusaio/module_spec/4C.json +129 -0
  170. velbusaio/module_spec/4D.json +108 -0
  171. velbusaio/module_spec/4E.json +787 -0
  172. velbusaio/module_spec/4F.json +114 -0
  173. velbusaio/module_spec/50.json +114 -0
  174. velbusaio/module_spec/51.json +114 -0
  175. velbusaio/module_spec/52.json +456 -0
  176. velbusaio/module_spec/54.json +270 -0
  177. velbusaio/module_spec/55.json +270 -0
  178. velbusaio/module_spec/56.json +270 -0
  179. velbusaio/module_spec/57.json +260 -0
  180. velbusaio/module_spec/5A.json +4 -0
  181. velbusaio/module_spec/5B.json +4 -0
  182. velbusaio/module_spec/5C.json +90 -0
  183. velbusaio/module_spec/5F.json +78 -0
  184. velbusaio/module_spec/60.json +4 -0
  185. velbusaio/module_spec/61.json +89 -0
  186. velbusaio/module_spec/broadcast.json +67 -0
  187. velbusaio/module_spec/ignore.json +22 -0
  188. velbusaio/protocol.py +243 -0
  189. velbusaio/py.typed +0 -0
  190. velbusaio/raw_message.py +149 -0
  191. velbusaio/util.py +55 -0
  192. velbusaio/vlp_reader.py +249 -0
  193. velbus_aio-2021.8.7.dist-info/METADATA +0 -66
  194. velbus_aio-2021.8.7.dist-info/RECORD +0 -90
  195. velbus_aio-2021.8.7.dist-info/top_level.txt +0 -1
  196. velbusaio/messages/meteo_raw.py +0 -52
  197. velbusaio/module_registry.py +0 -64
  198. velbusaio/moduleprotocol/protocol.json +0 -25540
  199. velbusaio/parser.py +0 -142
  200. {velbus_aio-2021.8.7.dist-info → velbus_aio-2025.11.0.dist-info/licenses}/LICENSE +0 -0
velbusaio/module.py CHANGED
@@ -1,30 +1,51 @@
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
- import pickle
12
+ import pathlib
9
13
  import struct
10
14
  import sys
15
+ from typing import Awaitable, Callable
16
+
17
+ from aiofile import async_open
11
18
 
12
19
  from velbusaio.channels import (
13
20
  Blind,
14
21
  Button,
15
22
  ButtonCounter,
23
+ Channel,
16
24
  Dimmer,
17
25
  EdgeLit,
18
26
  LightSensor,
19
27
  Memo,
20
28
  Relay,
29
+ SelectedProgram,
21
30
  Sensor,
22
31
  SensorNumber,
23
- Temperature,
32
+ )
33
+ from velbusaio.channels import Temperature
34
+ from velbusaio.channels import Temperature as TemperatureChannelType
35
+ from velbusaio.channels import (
24
36
  ThermostatChannel,
25
37
  )
26
- from velbusaio.const import PRIORITY_LOW
27
- from velbusaio.helpers import get_cache_dir, handle_match, keys_exists
38
+ from velbusaio.command_registry import commandRegistry
39
+ from velbusaio.const import (
40
+ CHANNEL_LIGHT_VALUE,
41
+ CHANNEL_MEMO_TEXT,
42
+ CHANNEL_SELECTED_PROGRAM,
43
+ PRIORITY_LOW,
44
+ SCAN_MODULEINFO_TIMEOUT_INITIAL,
45
+ )
46
+ from velbusaio.helpers import h2, handle_match, keys_exists
47
+ from velbusaio.message import Message
48
+ from velbusaio.messages.blind_status import BlindStatusMessage, BlindStatusNgMessage
28
49
  from velbusaio.messages.channel_name_part1 import (
29
50
  ChannelNamePart1Message,
30
51
  ChannelNamePart1Message2,
@@ -40,24 +61,42 @@ from velbusaio.messages.channel_name_part3 import (
40
61
  ChannelNamePart3Message2,
41
62
  ChannelNamePart3Message3,
42
63
  )
43
- from velbusaio.messages.channel_name_request import ChannelNameRequestMessage
64
+ from velbusaio.messages.channel_name_request import (
65
+ COMMAND_CODE as CHANNEL_NAME_REQUEST_COMMAND_CODE,
66
+ )
67
+ from velbusaio.messages.channel_name_request import (
68
+ ChannelNameRequestMessage,
69
+ )
44
70
  from velbusaio.messages.clear_led import ClearLedMessage
45
71
  from velbusaio.messages.counter_status import CounterStatusMessage
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
75
+ from velbusaio.messages.dali_device_settings import DeviceType as DaliDeviceType
76
+ from velbusaio.messages.dali_device_settings import DeviceTypeMsg as DaliDeviceTypeMsg
77
+ from velbusaio.messages.dali_device_settings import MemberOfGroupMsg
78
+ from velbusaio.messages.dali_device_settings_request import (
79
+ COMMAND_CODE as DALI_DEVICE_SETTINGS_REQUEST_COMMAND_CODE,
80
+ )
81
+ from velbusaio.messages.dali_device_settings_request import (
82
+ DaliDeviceSettingsRequest,
83
+ )
84
+ from velbusaio.messages.dali_dim_value_status import DimValueStatus
46
85
  from velbusaio.messages.dimmer_channel_status import DimmerChannelStatusMessage
47
86
  from velbusaio.messages.dimmer_status import DimmerStatusMessage
48
87
  from velbusaio.messages.fast_blinking_led import FastBlinkingLedMessage
49
88
  from velbusaio.messages.memory_data import MemoryDataMessage
50
89
  from velbusaio.messages.module_status import (
90
+ ModuleStatusGP4PirMessage,
51
91
  ModuleStatusMessage,
52
92
  ModuleStatusMessage2,
53
93
  ModuleStatusPirMessage,
54
94
  )
55
95
  from velbusaio.messages.module_status_request import ModuleStatusRequestMessage
56
- from velbusaio.messages.module_subtype import ModuleSubTypeMessage
57
- from velbusaio.messages.module_type import ModuleTypeMessage
58
96
  from velbusaio.messages.push_button_status import PushButtonStatusMessage
97
+ from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
59
98
  from velbusaio.messages.read_data_from_memory import ReadDataFromMemoryMessage
60
- from velbusaio.messages.relay_status import RelayStatusMessage
99
+ from velbusaio.messages.relay_status import RelayStatusMessage, RelayStatusMessage2
61
100
  from velbusaio.messages.sensor_temperature import SensorTemperatureMessage
62
101
  from velbusaio.messages.set_led import SetLedMessage
63
102
  from velbusaio.messages.slider_status import SliderStatusMessage
@@ -71,19 +110,51 @@ class Module:
71
110
  Abstract class for Velbus hardware modules.
72
111
  """
73
112
 
113
+ @classmethod
114
+ def factory(
115
+ cls,
116
+ module_address: int,
117
+ module_type: int,
118
+ serial: int | None = None,
119
+ memorymap: int | None = None,
120
+ build_year: int | None = None,
121
+ build_week: int | None = None,
122
+ cache_dir: str | None = None,
123
+ ) -> Module:
124
+ if module_type == 0x45 or module_type == 0x5A:
125
+ return VmbDali(
126
+ module_address,
127
+ module_type,
128
+ serial,
129
+ memorymap,
130
+ build_year,
131
+ build_week,
132
+ cache_dir,
133
+ )
134
+
135
+ return Module(
136
+ module_address,
137
+ module_type,
138
+ serial,
139
+ memorymap,
140
+ build_year,
141
+ build_week,
142
+ cache_dir,
143
+ )
144
+
74
145
  def __init__(
75
146
  self,
76
147
  module_address: int,
77
148
  module_type: int,
78
- module_data: dict,
79
- serial=None,
80
- memorymap=None,
81
- build_year=None,
82
- build_week=None,
149
+ serial: int | None = None,
150
+ memorymap: int | None = None,
151
+ build_year: int | None = None,
152
+ build_week: int | None = None,
153
+ cache_dir: str | None = None,
83
154
  ) -> None:
84
155
  self._address = module_address
85
- self._type = module_type
86
- self._data = module_data
156
+ self._type = int(module_type)
157
+ self._data = {}
87
158
 
88
159
  self._name = {}
89
160
  self._sub_address = {}
@@ -91,31 +162,79 @@ class Module:
91
162
  self.memory_map_version = memorymap
92
163
  self.build_year = build_year
93
164
  self.build_week = build_week
165
+ self._cache_dir = cache_dir
94
166
  self._is_loading = False
167
+ self._got_status = asyncio.Event()
168
+ self._got_status.clear()
95
169
  self._channels = {}
96
170
  self.loaded = False
171
+ self._use_cache = True
172
+ self._loaded_cache = {}
97
173
 
98
- def initialize(self, writer: type) -> None:
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}")
179
+
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:
99
184
  self._log = logging.getLogger("velbus-module")
100
- self._log.setLevel(logging.DEBUG)
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
101
208
  self._writer = writer
102
209
  for chan in self._channels.values():
103
210
  chan._writer = writer
104
211
 
105
212
  def cleanupSubChannels(self) -> None:
106
- if self._sub_address == {}:
107
- assert "No subaddresses defined"
213
+ # TODO: 21/11/2022 DannyDeGaspari: Fix needed
214
+ # Care should be taken for this function, not all subaddresses have their channels on multiples of 8.
215
+ # The last subaddress contain typically the temperature channels, has more then 8 channels
216
+ # and doesn't start on a boundary of 8.
217
+ # E.g. The VMBGP4 has one subaddress, so since the second subaddress is not defined,
218
+ # this function will delete channels 17-24 while 17 and 18 belong to the temperature channels.
219
+ #
220
+ # The solution would be that this functions knows were the temperature channels are located
221
+ # and/or what the max number of subaddresses are for each module.
222
+ # if self._sub_address == {} and self.loaded:
223
+ # raise Exception("No subaddresses defined")
108
224
  for sub in range(1, 4):
109
225
  if sub not in self._sub_address:
110
226
  for i in range(((sub * 8) + 1), (((sub + 1) * 8) + 1)):
111
- if i in self._channels:
227
+ if i in self._channels and not isinstance(
228
+ self._channels[i], TemperatureChannelType
229
+ ):
112
230
  del self._channels[i]
113
231
 
114
- def _cache(self) -> None:
115
- if not os.path.isdir(get_cache_dir()):
116
- os.mkdir(get_cache_dir())
117
- with open(f"{get_cache_dir()}/{self._address}.p", "wb") as fl:
118
- 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))
119
238
 
120
239
  def __getstate__(self) -> dict:
121
240
  d = self.__dict__
@@ -126,20 +245,20 @@ class Module:
126
245
  self.__dict__ = state
127
246
 
128
247
  def __repr__(self) -> str:
129
- return (
130
- "<{}: {{{}}} @ {{{}}} loaded:{{{}}} loading:{{{}}} channels{{:{}}}>".format(
131
- self._name,
132
- self._type,
133
- self._address,
134
- self.loaded,
135
- self._is_loading,
136
- self._channels,
137
- )
138
- )
248
+ return f"<{self._name} type:{self._type} address:{self._address} loaded:{self.loaded} loading:{self._is_loading} channels: {self._channels}>"
139
249
 
140
250
  def __str__(self) -> str:
141
251
  return self.__repr__()
142
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
+
143
262
  def get_addresses(self) -> list:
144
263
  """
145
264
  Get all addresses for this module
@@ -157,26 +276,35 @@ class Module:
157
276
  return self._type
158
277
 
159
278
  def get_type_name(self) -> str:
160
- return self._data["Type"]
279
+ if "Type" in self._data:
280
+ return self._data["Type"]
281
+ return "UNKNOWN"
161
282
 
162
- def get_serial(self) -> str:
283
+ def get_serial(self) -> str | None:
163
284
  return self.serial
164
285
 
165
286
  def get_name(self) -> str:
166
287
  return self._name
167
288
 
168
289
  def get_sw_version(self) -> str:
169
- return "{}-{}.{}.{}".format(
170
- self.serial,
171
- self.memory_map_version,
172
- self.build_year,
173
- self.build_week,
174
- )
175
-
176
- async def on_message(self, message) -> None:
290
+ return f"{self.serial}-{self.memory_map_version}.{self.build_year}.{self.build_week}"
291
+
292
+ def calc_channel_offset(self, address: int) -> int:
293
+ _channel_offset = 0
294
+ if self._address != address:
295
+ for _sub_addr_key, _sub_addr_val in self._sub_address.items():
296
+ if _sub_addr_val == address:
297
+ _channel_offset = 8 * _sub_addr_key
298
+ break
299
+ return _channel_offset
300
+
301
+ async def on_message(self, message: Message) -> None:
177
302
  """
178
303
  Process received message
179
304
  """
305
+ self._log.debug(f"RX: {message}")
306
+ _channel_offset = self.calc_channel_offset(message.address)
307
+
180
308
  if isinstance(
181
309
  message,
182
310
  (
@@ -186,6 +314,7 @@ class Module:
186
314
  ),
187
315
  ):
188
316
  self._process_channel_name_message(1, message)
317
+ await self._cache()
189
318
  elif isinstance(
190
319
  message,
191
320
  (
@@ -195,6 +324,7 @@ class Module:
195
324
  ),
196
325
  ):
197
326
  self._process_channel_name_message(2, message)
327
+ await self._cache()
198
328
  elif isinstance(
199
329
  message,
200
330
  (
@@ -204,135 +334,314 @@ class Module:
204
334
  ),
205
335
  ):
206
336
  self._process_channel_name_message(3, message)
337
+ await self._cache()
207
338
  elif isinstance(message, MemoryDataMessage):
208
339
  await self._process_memory_data_message(message)
209
- elif isinstance(message, RelayStatusMessage):
210
- await self._channels[message.channel].update({"on": message.is_on()})
340
+ elif isinstance(message, (RelayStatusMessage, RelayStatusMessage2)):
341
+ await self._update_channel(
342
+ message.channel,
343
+ {
344
+ "on": message.is_on(),
345
+ "inhibit": message.is_inhibited(),
346
+ "forced_on": message.is_forced_on(),
347
+ "disabled": message.is_disabled(),
348
+ },
349
+ )
211
350
  elif isinstance(message, SensorTemperatureMessage):
212
351
  chan = self._translate_channel_name(self._data["TemperatureChannel"])
213
- await self._channels[chan].update(
352
+ await self._channels[chan].maybe_update_temperature(
353
+ message.getCurTemp(), 1 / 64
354
+ )
355
+ await self._update_channel(
356
+ chan,
214
357
  {
215
- "cur": message.getCurTemp(),
216
358
  "min": message.getMinTemp(),
217
359
  "max": message.getMaxTemp(),
218
- }
360
+ },
219
361
  )
220
362
  elif isinstance(message, TempSensorStatusMessage):
221
363
  # update the current temp
222
364
  chan = self._translate_channel_name(self._data["TemperatureChannel"])
223
365
  if chan in self._channels:
224
- await self._channels[chan].update({"cur": message.current_temp})
225
- # self._target = message.target_temp
226
- # self._cmode = message.mode_str
227
- # self._cstatus = message.status_str
366
+ await self._update_channel(
367
+ chan,
368
+ {
369
+ "target": message.target_temp,
370
+ "cmode": message.mode_str,
371
+ "cstatus": message.status_str,
372
+ "sleep_timer": message.sleep_timer,
373
+ "cool_mode": message.cool_mode,
374
+ },
375
+ )
376
+ await self._channels[chan].maybe_update_temperature(
377
+ message.current_temp, 1 / 2
378
+ )
379
+ # update the thermostat channels
380
+ channel_name_to_msg_prop_map = {
381
+ "Heater": "heater",
382
+ "Boost": "boost",
383
+ "Pump": "pump",
384
+ "Cooler": "cooler",
385
+ "Alarm 1": "alarm1",
386
+ "Alarm 2": "alarm2",
387
+ "Alarm 3": "alarm3",
388
+ "Alarm 4": "alarm4",
389
+ }
390
+ for channel_str in self._data["Channels"]:
391
+ if keys_exists(self._data, "Channels", channel_str, "Type"):
392
+ if (
393
+ self._data["Channels"][channel_str]["Type"]
394
+ == "ThermostatChannel"
395
+ ):
396
+ channel = self._translate_channel_name(channel_str)
397
+ channel_name = self._data["Channels"][channel_str]["Name"]
398
+ if channel in self._channels:
399
+ await self._update_channel(
400
+ channel,
401
+ {
402
+ "closed": getattr(
403
+ message,
404
+ channel_name_to_msg_prop_map[channel_name],
405
+ )
406
+ },
407
+ )
228
408
  elif isinstance(message, PushButtonStatusMessage):
229
- for channel in message.closed:
230
- await self._channels[channel].update({"closed": True})
231
- for channel in message.opened:
232
- await self._channels[channel].update({"closed": False})
233
- elif isinstance(message, ModuleStatusMessage):
234
- for channel in self._channels.keys():
235
- if channel in message.closed:
236
- await self._channels[channel].update({"closed": True})
237
- elif isinstance(self._channels[channel], (Button, ButtonCounter)):
238
- await self._channels[channel].update({"closed": False})
239
- elif isinstance(message, ModuleStatusMessage2):
240
- for channel in self._channels.keys():
241
- if channel in message.closed:
242
- await self._channels[channel].update({"closed": True})
243
- elif isinstance(self._channels[channel], (Button, ButtonCounter)):
244
- await self._channels[channel].update({"closed": False})
245
- if channel in message.enabled:
246
- await self._channels[channel].update({"enabled": True})
409
+ _update_buttons = False
410
+ for channel_types in self._data["Channels"]:
411
+ if keys_exists(self._data, "Channels", channel_types, "Type"):
412
+ if (
413
+ self._data["Channels"][channel_types]["Type"] == "Button"
414
+ or self._data["Channels"][channel_types]["Type"] == "Sensor"
415
+ or self._data["Channels"][channel_types]["Type"]
416
+ == "ButtonCounter"
417
+ ):
418
+ _update_buttons = True
419
+ break
420
+ if _update_buttons:
421
+ for channel_id in range(1, 9):
422
+ channel = self._translate_channel_name(channel_id + _channel_offset)
423
+ if channel_id in message.closed:
424
+ await self._update_channel(channel, {"closed": True})
425
+ if channel_id in message.closed_long:
426
+ await self._update_channel(channel, {"long": True})
427
+ if channel_id in message.opened:
428
+ await self._update_channel(
429
+ channel, {"closed": False, "long": False}
430
+ )
431
+ elif isinstance(message, (ModuleStatusMessage)):
432
+ for channel_id in range(1, 9):
433
+ channel = self._translate_channel_name(channel_id + _channel_offset)
434
+ if channel_id in message.closed:
435
+ await self._update_channel(channel, {"closed": True})
436
+ elif channel in self._channels and isinstance(
437
+ self._channels[channel], (Button, ButtonCounter)
438
+ ):
439
+ await self._update_channel(channel, {"closed": False})
440
+ elif isinstance(message, (ModuleStatusMessage2)):
441
+ for channel_id in range(1, 9):
442
+ channel = self._translate_channel_name(channel_id + _channel_offset)
443
+ if channel_id in message.closed:
444
+ await self._update_channel(channel, {"closed": True})
247
445
  elif isinstance(self._channels[channel], (Button, ButtonCounter)):
248
- await self._channels[channel].update({"enabled": False})
446
+ await self._update_channel(channel, {"closed": False})
447
+ if channel_id in message.enabled:
448
+ await self._update_channel(channel, {"enabled": True})
449
+ elif channel in self._channels and isinstance(
450
+ self._channels[channel], (Button, ButtonCounter)
451
+ ):
452
+ await self._update_channel(channel, {"enabled": False})
453
+ # self.selected_program_str = message.selected_program_str
454
+ await self._update_channel(
455
+ CHANNEL_SELECTED_PROGRAM,
456
+ {"selected_program_str": message.selected_program_str},
457
+ )
249
458
  elif isinstance(message, CounterStatusMessage) and isinstance(
250
459
  self._channels[message.channel], ButtonCounter
251
460
  ):
252
- await self._channels[message.channel].update(
461
+ channel = self._translate_channel_name(message.channel)
462
+ await self._update_channel(
463
+ channel,
253
464
  {
254
465
  "pulses": message.pulses,
255
466
  "counter": message.counter,
256
467
  "delay": message.delay,
257
- }
468
+ },
258
469
  )
259
470
  elif isinstance(message, ModuleStatusPirMessage):
260
- await self._channels[99].update({"cur": message.light_value})
471
+ await self._update_channel(
472
+ CHANNEL_LIGHT_VALUE, {"cur": message.light_value}
473
+ )
474
+ await self._update_channel(1, {"closed": message.dark})
475
+ await self._update_channel(2, {"closed": message.light})
476
+ await self._update_channel(3, {"closed": message.motion1})
477
+ await self._update_channel(4, {"closed": message.light_motion1})
478
+ await self._update_channel(5, {"closed": message.motion2})
479
+ await self._update_channel(6, {"closed": message.light_motion2})
480
+ if 7 in self._channels:
481
+ await self._update_channel(7, {"closed": message.low_temp_alarm})
482
+ if 8 in self._channels:
483
+ await self._update_channel(8, {"closed": message.high_temp_alarm})
484
+ # self.selected_program_str = message.selected_program_str
485
+ await self._update_channel(
486
+ CHANNEL_SELECTED_PROGRAM,
487
+ {"selected_program_str": message.selected_program_str},
488
+ )
489
+ elif isinstance(message, ModuleStatusGP4PirMessage):
490
+ await self._update_channel(
491
+ CHANNEL_LIGHT_VALUE, {"cur": message.light_value}
492
+ )
493
+ for channel_id in range(1, 9):
494
+ channel = self._translate_channel_name(channel_id + _channel_offset)
495
+ await self._update_channel(
496
+ channel, {"closed": channel_id in message.closed}
497
+ )
498
+ if type(self._channels[channel]) is Button:
499
+ # only treat 'enabled' if the channel is a Button
500
+ await self._update_channel(
501
+ channel, {"enabled": channel_id in message.enabled}
502
+ )
503
+ # self.selected_program_str = message.selected_program_str
504
+ await self._update_channel(
505
+ CHANNEL_SELECTED_PROGRAM,
506
+ {"selected_program_str": message.selected_program_str},
507
+ )
261
508
  elif isinstance(message, UpdateLedStatusMessage):
262
- for channel in self._channels.keys():
263
- if channel in message.led_slow_blinking:
264
- await self._channels[channel].update({"led_state": "slow"})
265
- if channel in message.led_fast_blinking:
266
- await self._channels[channel].update({"led_state": "fast"})
267
- if channel in message.led_on:
268
- await self._channels[channel].update({"led_state": "on"})
509
+ for channel_id in range(1, 9):
510
+ channel = self._translate_channel_name(channel_id + _channel_offset)
511
+ if channel_id in message.led_slow_blinking:
512
+ await self._update_channel(channel, {"led_state": "slow"})
513
+ if channel_id in message.led_fast_blinking:
514
+ await self._update_channel(channel, {"led_state": "fast"})
515
+ if channel_id in message.led_on:
516
+ await self._update_channel(channel, {"led_state": "on"})
269
517
  if (
270
- channel not in message.led_slow_blinking
271
- and channel not in message.led_fast_blinking
272
- and channel not in message.led_on
518
+ channel_id not in message.led_slow_blinking
519
+ and channel_id not in message.led_fast_blinking
520
+ and channel_id not in message.led_on
273
521
  ):
274
- await self._channels[channel].update({"led_state": "off"})
522
+ await self._update_channel(channel, {"led_state": "off"})
275
523
  elif isinstance(message, SetLedMessage):
276
- for channel in self._channels.keys():
277
- if channel in message.leds:
278
- await self._channels[channel].update({"led_state": "on"})
524
+ for channel_id in range(1, 9):
525
+ channel = self._translate_channel_name(channel_id + _channel_offset)
526
+ if channel_id in message.leds:
527
+ await self._update_channel(channel, {"led_state": "on"})
279
528
  elif isinstance(message, ClearLedMessage):
280
- for channel in self._channels.keys():
281
- if channel in message.leds:
282
- await self._channels[channel].update({"led_state": "off"})
529
+ for channel_id in range(1, 9):
530
+ channel = self._translate_channel_name(channel_id + _channel_offset)
531
+ if channel_id in message.leds:
532
+ await self._update_channel(channel, {"led_state": "off"})
283
533
  elif isinstance(message, SlowBlinkingLedMessage):
284
- for channel in self._channels.keys():
285
- if channel in message.leds:
286
- await self._channels[channel].update({"led_state": "slow"})
534
+ for channel_id in range(1, 9):
535
+ channel = self._translate_channel_name(channel_id + _channel_offset)
536
+ if channel_id in message.leds:
537
+ await self._update_channel(channel, {"led_state": "slow"})
287
538
  elif isinstance(message, FastBlinkingLedMessage):
288
- for channel in self._channels.keys():
289
- if channel in message.leds:
290
- await self._channels[channel].update({"led_state": "fast"})
291
- elif isinstance(message, DimmerChannelStatusMessage):
292
- await self._channels[message.channel].update(
293
- {"state": message.cur_dimmer_state()}
294
- )
539
+ for channel_id in range(1, 9):
540
+ channel = self._translate_channel_name(channel_id + _channel_offset)
541
+ if channel_id in message.leds:
542
+ await self._update_channel(channel, {"led_state": "fast"})
543
+ elif isinstance(message, (DimmerChannelStatusMessage, DimmerStatusMessage)):
544
+ channel = self._translate_channel_name(message.channel)
545
+ await self._update_channel(channel, {"state": message.cur_dimmer_state()})
295
546
  elif isinstance(message, SliderStatusMessage):
296
- await self._channels[message.channel].update(
297
- {"state": message.cur_slider_state()}
547
+ channel = self._translate_channel_name(message.channel)
548
+ await self._update_channel(channel, {"state": message.cur_slider_state()})
549
+ elif isinstance(message, BlindStatusNgMessage):
550
+ channel = self._translate_channel_name(message.channel)
551
+ await self._update_channel(
552
+ channel, {"state": message.status, "position": message.position}
553
+ )
554
+ elif isinstance(message, BlindStatusMessage):
555
+ channel = self._translate_channel_name(message.channel)
556
+ await self._update_channel(channel, {"state": message.status})
557
+ elif isinstance(message, MeteoRawMessage):
558
+ await self._update_channel(11, {"cur": message.rain})
559
+ await self._update_channel(12, {"cur": message.light})
560
+ await self._update_channel(13, {"cur": message.wind})
561
+ elif isinstance(message, SensorRawMessage):
562
+ await self._update_channel(
563
+ message.sensor, {"cur": message.value, "unit": message.unit}
298
564
  )
299
- elif isinstance(message, DimmerStatusMessage):
300
- await self._channels[message.channel].update(
301
- {"state": message.cur_dimmer_state()}
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()
575
+
576
+ async def _update_channel(self, channel: int, updates: dict):
577
+ try:
578
+ await self._channels[channel].update(updates)
579
+ except KeyError:
580
+ self._log.info(
581
+ f"channel {channel} does not exist for module @ address {self}"
302
582
  )
303
- self._cache()
304
583
 
305
- def get_channels(self) -> list:
584
+ def get_channels(self) -> dict:
306
585
  """
307
586
  List all channels for this module
308
587
  """
309
588
  return self._channels
310
589
 
311
- async def load(self) -> None:
312
- """
313
- Retrieve names of channels
314
- """
315
- # did we already start the loading?
316
- # this is needed for the submodules,
317
- # as the submodule address maps to the main module
318
- # this method can be called multiple times
319
- if self._is_loading or self.loaded:
320
- return
321
- self._log.info("Load Module")
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
+
602
+ async def load(self, from_cache: bool = False) -> None:
322
603
  # start the loading
323
604
  self._is_loading = True
605
+ # see if we have a cache
606
+ cache = await self._get_cache()
607
+ self._loaded_cache = cache
324
608
  # load default channels
325
- self.__load_default_channels()
609
+ await self._load_default_channels()
610
+
326
611
  # load the data from memory ( the stuff that we need)
327
- await self.__load_memory()
612
+ if "name" in cache and cache["name"] != "":
613
+ self._name = cache["name"]
614
+ else:
615
+ await self.__load_memory()
328
616
  # load the module status
329
- await self._request_module_status()
617
+ # await self._request_module_status()
330
618
  # load the channel names
331
- 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()
332
631
  # load the module specific stuff
333
632
  self._load()
334
633
  # stop the loading
335
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
336
645
 
337
646
  def _load(self) -> None:
338
647
  """
@@ -350,31 +659,50 @@ class Module:
350
659
  return 0
351
660
  return max(self._channels.keys())
352
661
 
353
- async def _process_memory_data_message(self, message) -> None:
662
+ async def set_memo_text(self, txt: str) -> None:
663
+ if CHANNEL_MEMO_TEXT not in self._channels.keys():
664
+ return
665
+ await self._channels[CHANNEL_MEMO_TEXT].set(txt)
666
+
667
+ async def _process_memory_data_message(self, message: MemoryDataMessage) -> None:
354
668
  addr = "{high:02X}{low:02X}".format(
355
669
  high=message.high_address, low=message.low_address
356
670
  )
357
- try:
358
- mdata = self._data["Memory"]["1"]["Address"][addr]
359
- if "ModuleName" in mdata and isinstance(self._name, dict):
360
- # if self._name is a dict we are still loading
361
- # if its a string it was already complete
362
- if message.data == 0xFF:
363
- # modulename is complete
364
- self._name = "".join(str(x) for x in self._name.values())
365
- else:
366
- char = mdata["ModuleName"].split(":")[0]
367
- self._name[int(char)] = chr(message.data)
368
- elif "Match" in mdata:
369
- for chan, chan_data in handle_match(
370
- mdata["Match"], message.data
371
- ).items():
372
- data = chan_data.copy()
373
- await self._channels[chan].update(data)
374
- except KeyError:
375
- print("KEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEY")
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]
676
+ if "ModuleName" in mdata and isinstance(self._name, dict):
677
+ # if self._name is a dict we are still loading
678
+ # if its a string it was already complete
679
+ char_and_save = mdata["ModuleName"].split(":")
680
+ char = char_and_save[0]
681
+ self._name[int(char)] = chr(message.data)
682
+ if len(char_and_save) > 1 and char_and_save[1] == "Save":
683
+ self._name = "".join(
684
+ str(x) for x in self._name.values() if x != chr(0xFF)
685
+ )
686
+ elif "Match" in mdata:
687
+ for chan, chan_data in handle_match(mdata["Match"], message.data).items():
688
+ data = chan_data.copy()
689
+ await self._update_channel(chan, data)
690
+ elif "SensorName" in mdata:
691
+ # this is part of the channel names
692
+ # make sure we set the channel to loaded
693
+ # format of the value (in mdata)
694
+ # channel:char:start/save
695
+ spl = mdata["SensorName"].split(":")
696
+ if len(spl) == 2:
697
+ [chan, pos] = spl
698
+ elif len(spl) == 3:
699
+ [chan, pos, dummy] = spl
700
+ chan = self._translate_channel_name(chan)
701
+ self._channels[chan].set_name_char(pos, message.data)
702
+ else:
703
+ self._log.debug(mdata)
376
704
 
377
- def _process_channel_name_message(self, part, message) -> None:
705
+ def _process_channel_name_message(self, part: int, message: Message) -> None:
378
706
  channel = self._translate_channel_name(message.channel)
379
707
  if channel not in self._channels:
380
708
  return
@@ -393,7 +721,7 @@ class Module:
393
721
  )
394
722
  return int(channel)
395
723
 
396
- def is_loaded(self) -> bool:
724
+ async def is_loaded(self) -> bool:
397
725
  """
398
726
  Check if all name messages have been received
399
727
  """
@@ -411,14 +739,31 @@ class Module:
411
739
  return False
412
740
  # set that we finished the module loading
413
741
  self.loaded = True
414
- self._cache()
742
+ await self._cache()
415
743
  return True
416
744
 
417
745
  async def _request_module_status(self) -> None:
418
- # request the module status (if available for this module
419
- msg = ModuleStatusRequestMessage(self._address)
420
- msg.channels = list(range(1, 9))
421
- await self._writer(msg)
746
+ """Request current state of channels."""
747
+ if "Channels" not in self._data:
748
+ # some modules have no channels
749
+ return
750
+ self._log.info(f"Request module status {self._address}")
751
+
752
+ mod_stat_req_msg = ModuleStatusRequestMessage(self._address)
753
+ counter_msg = None
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))
764
+ await self._writer(mod_stat_req_msg)
765
+ if counter_msg is not None:
766
+ await self._writer(counter_msg)
422
767
 
423
768
  async def _request_channel_name(self) -> None:
424
769
  # request the module channel names
@@ -428,7 +773,10 @@ class Module:
428
773
  msg.channels = 0xFF
429
774
  await self._writer(msg)
430
775
  else:
431
- msg = ChannelNameRequestMessage(self._address)
776
+ msg_type = commandRegistry.get_command(
777
+ CHANNEL_NAME_REQUEST_COMMAND_CODE, self.get_type()
778
+ )
779
+ msg = msg_type(self._address)
432
780
  msg.priority = PRIORITY_LOW
433
781
  msg.channels = list(range(1, (self.number_of_channels() + 1)))
434
782
  await self._writer(msg)
@@ -441,9 +789,14 @@ class Module:
441
789
  self._name = None
442
790
  return
443
791
 
444
- for _memory_key, memory_part in self._data["Memory"].items():
445
- if "Address" in memory_part:
446
- for addr_int in memory_part["Address"].keys():
792
+ if self._type == 0x0C:
793
+ self._name = None
794
+ return
795
+
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():
447
800
  addr = struct.unpack(
448
801
  ">BB", struct.pack(">h", int("0x" + addr_int, 0))
449
802
  )
@@ -453,15 +806,184 @@ class Module:
453
806
  msg.low_address = addr[1]
454
807
  await self._writer(msg)
455
808
 
456
- def __load_default_channels(self) -> None:
809
+ async def _load_default_channels(self) -> None:
457
810
  if "Channels" not in self._data:
458
811
  return
459
812
 
460
813
  for chan, chan_data in self._data["Channels"].items():
461
814
  edit = True
815
+ sub = True
462
816
  if "Editable" not in chan_data or chan_data["Editable"] != "yes":
463
817
  edit = False
818
+ if "Subdevice" not in chan_data or chan_data["Subdevice"] != "yes":
819
+ sub = False
464
820
  cls = getattr(sys.modules[__name__], chan_data["Type"])
465
821
  self._channels[int(chan)] = cls(
466
- 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,
829
+ )
830
+ if chan_data["Type"] == "Temperature":
831
+ if "Thermostat" in self._data or (
832
+ "ThermostatAddr" in self._data and self._data["ThermostatAddr"] != 0
833
+ ):
834
+ await self._update_channel(int(chan), {"thermostat": True})
835
+ if chan_data["Type"] == "Dimmer" and "sliderScale" in self._data:
836
+ self._channels[int(chan)].slider_scale = self._data["sliderScale"]
837
+
838
+
839
+ class VmbDali(Module):
840
+ """
841
+ DALI has a variable number of channels: the number of channels
842
+ depends on the number of DALI devices on the DALI bus
843
+ """
844
+
845
+ def __init__(
846
+ self,
847
+ module_address: int,
848
+ module_type: int,
849
+ serial: int | None = None,
850
+ memorymap: int | None = None,
851
+ build_year: int | None = None,
852
+ build_week: int | None = None,
853
+ cache_dir: str | None = None,
854
+ ) -> None:
855
+ super().__init__(
856
+ module_address,
857
+ module_type,
858
+ serial,
859
+ memorymap,
860
+ build_year,
861
+ build_week,
862
+ cache_dir,
863
+ )
864
+ self.group_members: dict[int, set[int]] = {}
865
+
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,
467
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()
884
+
885
+ async def _request_dali_channels(self):
886
+ msg_type = commandRegistry.get_command(
887
+ DALI_DEVICE_SETTINGS_REQUEST_COMMAND_CODE, self.get_type()
888
+ )
889
+ msg: DaliDeviceSettingsRequest = msg_type(self._address)
890
+ msg.priority = PRIORITY_LOW
891
+ msg.channel = 81 # all
892
+ msg.settings = None # all
893
+ await self._writer(msg)
894
+
895
+ async def on_message(self, message: Message) -> None:
896
+ if isinstance(message, DaliDeviceSettingMsg):
897
+ if isinstance(message.data, DaliDeviceTypeMsg):
898
+ if message.data.device_type == DaliDeviceType.NoDevicePresent:
899
+ if message.channel in self._channels:
900
+ del self._channels[message.channel]
901
+ elif message.data.device_type == DaliDeviceType.LedModule:
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:
921
+ # New or changed type, replace channel:
922
+ self._channels[message.channel] = Dimmer(
923
+ self,
924
+ message.channel,
925
+ None,
926
+ True,
927
+ True,
928
+ self._writer,
929
+ self._address,
930
+ slider_scale=254,
931
+ )
932
+ await self._request_single_channel_name(message.channel)
933
+
934
+ elif isinstance(message.data, MemberOfGroupMsg):
935
+ for group in range(0, 15 + 1):
936
+ this_group_members = self.group_members.setdefault(group, set())
937
+ if message.data.member_of_group[group]:
938
+ this_group_members.add(message.channel)
939
+ elif message.channel in this_group_members:
940
+ this_group_members.remove(message.channel)
941
+
942
+ elif isinstance(message, PushButtonStatusMessage):
943
+ _channel_offset = self.calc_channel_offset(message.address)
944
+ for channel in message.opened:
945
+ if _channel_offset + channel > 64: # ignore groups
946
+ continue
947
+ await self._update_channel((_channel_offset + channel), {"state": 0})
948
+ # ignore message.closed: we don't know at what dimlevel they're started
949
+
950
+ elif isinstance(message, DimValueStatus):
951
+ for offset, dim_value in enumerate(message.dim_values):
952
+ channel = message.channel + offset
953
+ if channel <= 64: # channel
954
+ await self._update_channel(channel, {"state": dim_value})
955
+ elif channel <= 80: # group
956
+ group_num = channel - 65
957
+ for chan in self.group_members.get(group_num, []):
958
+ await self._update_channel(chan, {"state": dim_value})
959
+ else: # broadcast
960
+ for chan in self._channels.values():
961
+ await chan.update({"state": dim_value})
962
+
963
+ elif isinstance(
964
+ message,
965
+ (
966
+ SetLedMessage,
967
+ ClearLedMessage,
968
+ FastBlinkingLedMessage,
969
+ SlowBlinkingLedMessage,
970
+ ),
971
+ ):
972
+ pass
973
+
974
+ else:
975
+ return await super().on_message(message)
976
+
977
+ async def _request_channel_name(self) -> None:
978
+ # Channel names are requested after channel scan
979
+ # don't do them here (at initialization time)
980
+ pass
981
+
982
+ async def _request_single_channel_name(self, channel_num: int) -> None:
983
+ msg_type = commandRegistry.get_command(
984
+ CHANNEL_NAME_REQUEST_COMMAND_CODE, self.get_type()
985
+ )
986
+ msg = msg_type(self._address)
987
+ msg.priority = PRIORITY_LOW
988
+ msg.channels = channel_num
989
+ await self._writer(msg)