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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: velbus-aio
3
- Version: 2024.5.1
3
+ Version: 2024.7.0
4
4
  Summary: Open-source home automation platform running on Python 3.
5
5
  Author-email: Maikel Punie <maikel.punie@gmail.com>
6
6
  License: MIT
@@ -1,16 +1,16 @@
1
1
  velbusaio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- velbusaio/channels.py,sha256=1fqxsNHmJmb6lYRr4UqP9HLeTprbvektzrjQ8QBvWi0,22889
2
+ velbusaio/channels.py,sha256=YdIMneLYj_zTa8e2dHmhZvGKcD3iyv5jMeZpfuORcTU,22910
3
3
  velbusaio/command_registry.py,sha256=j9KuLmada41PMEdtfPvLmXVOAZOkBM9zi-3kluwYffk,5140
4
- velbusaio/const.py,sha256=aysTlQcpACZaDVqoXAjWmaEDGlCB7KT04Rv-SAusPFc,1575
5
- velbusaio/controller.py,sha256=txOd0JuYi_OGMzO1t2u7KXlF085HWa1HVlszz7A2O5c,8846
4
+ velbusaio/const.py,sha256=HyBLMJkzZ0ApJRjCffcWlSGr2c3Qgt6kFpdhLQwl--c,1862
5
+ velbusaio/controller.py,sha256=XcASgSs5DBwmjAjV3Aa9L4wyJEuLpn8qEUqwfWNkTPA,7369
6
6
  velbusaio/discovery.py,sha256=Px6qoZl4QhF17aMz6JxstCORBpLzZGWEK9h4Vyvg57o,1649
7
7
  velbusaio/exceptions.py,sha256=FHkXaM3dK5Gkk-QGAf9dLE3FPlCU2FRZWUyY-4KRNnA,515
8
- velbusaio/handler.py,sha256=jTWIV26ibAqs3rcTku81NIHlPUeyWjStSih1Nfx-an0,5946
8
+ velbusaio/handler.py,sha256=UF-F2LeVEocDniXKLE8FmUMO-o9QdRtDQoYog6d4rag,10867
9
9
  velbusaio/helpers.py,sha256=iqpoereRH4JY5WAkozIqWvXWyRmhko-J-UGXDylFyEM,2537
10
- velbusaio/message.py,sha256=gnhXpA5NAG4qnI9vlxwlWQKETWneDb1lp0P9w_-nxhQ,5020
11
- velbusaio/module.py,sha256=_yFxZIi71eqq9xxNal-9ATNHGMjZDOr-dcpKYIxa7_8,35458
12
- velbusaio/protocol.json,sha256=7PifRZXctU5zSsodDGsUAhnnSsGNlcGcim1gr2su720,250396
13
- velbusaio/protocol.py,sha256=ofDwJfwyrVQDQ40WX2QdH1nTK-y2HibQPTo5dFY7SkQ,8224
10
+ velbusaio/message.py,sha256=_MRuI4XnMcqeduCDJ3AwhAPL1B8VMstFDnjjLo8QuxM,5018
11
+ velbusaio/module.py,sha256=OMFIa8VhJR-G_rg2PcGUdpmubLD6gDwVruN4ZCmwpMY,35215
12
+ velbusaio/protocol.json,sha256=cQ2RsXomOjB-sd4fIu53QthOlkcfJSa7M0oGkzi9fJQ,253142
13
+ velbusaio/protocol.py,sha256=3XhSFYe2GS-3fis5uClJV4tJxI_XO0x-JKZW0cD5cKs,8004
14
14
  velbusaio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  velbusaio/raw_message.py,sha256=ODoTPVAvyXXQSMXxtQO1U5OxcoQ4W8JJN5fotEDGSTo,4690
16
16
  velbusaio/util.py,sha256=FW6YCiPYWOCgqHDs8-LbzME0h81mqftYVG0qqZ-oo7Y,1568
@@ -51,8 +51,8 @@ velbusaio/messages/memory_data_block.py,sha256=3QufrOahEkIhJjivWtOXp62YuVaUwx6tr
51
51
  velbusaio/messages/memory_dump_request.py,sha256=xrPsdpygD9DUF3lp-BzJacuq1dNkqz2wtHpqu_F1zVM,699
52
52
  velbusaio/messages/module_status.py,sha256=83UrRFXgRRngcF36fogyw8G5XPFlyaoZoW7XlxwvnQE,6637
53
53
  velbusaio/messages/module_status_request.py,sha256=s3h8F6Dpmjcb4hqDQKIzdOgocW1F_st40cctcW7WmQ4,961
54
- velbusaio/messages/module_subtype.py,sha256=VQXQ-c_yFbj9_64SjfiM-PLk1hV7xUWIcQ1DZYTpKhI,1464
55
- velbusaio/messages/module_type.py,sha256=toHlsLoek61cQ9JXG3alJyAuQtny1RSY7K6cM4hfNOk,4116
54
+ velbusaio/messages/module_subtype.py,sha256=KG-OC0eW-ZRnP1P8EWmcay6O-no6Mn2cklEjokaIW9M,1487
55
+ velbusaio/messages/module_type.py,sha256=fb4q_SP3B6h_f9jGxgMvNoIpgBPXx36nwjCmtf0DfBw,4154
56
56
  velbusaio/messages/module_type_request.py,sha256=fntMYCF1WVwSseCy3zRoZQH6JeGz8JDfTkwHC3AvcB4,754
57
57
  velbusaio/messages/push_button_status.py,sha256=SRFLvNC00FdKDTSb4kNVSHeXI1i5Ey5ENxpsB3B8FuU,1554
58
58
  velbusaio/messages/raw.py,sha256=pepZSUVY-ro5jv3ZyO6BI3ap5RZ2g_Bd9dlbaICFldQ,1964
@@ -96,8 +96,8 @@ velbusaio/messages/very_fast_blinking_led.py,sha256=vlMEern8PoOvtO5JaAk9erMR4IKJ
96
96
  velbusaio/messages/write_data_to_memory.py,sha256=gr6bi4SzK8Mw8fnp8yV-STq5jts7NoeV7zZgdptH5Vs,1039
97
97
  velbusaio/messages/write_memory_block.py,sha256=zGnNxx_M66HpBQ8S7kagtNw8_qSRHsOLk1MuiS0uygM,1032
98
98
  velbusaio/messages/write_module_address_and_serial_number.py,sha256=6y57j-md3btNtQddX5CUREtSs1Dzgkd953sQPZ3Pioo,1597
99
- velbus_aio-2024.5.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
100
- velbus_aio-2024.5.1.dist-info/METADATA,sha256=z1Q-cGFQfsDQYGHAppFwXJUDplFi6qaFKIGJW0Me3c4,3259
101
- velbus_aio-2024.5.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
102
- velbus_aio-2024.5.1.dist-info/top_level.txt,sha256=W0-lSOwD23mm8FqaIe9vY20fKicBMIdUVjF-zmfxRnY,15
103
- velbus_aio-2024.5.1.dist-info/RECORD,,
99
+ velbus_aio-2024.7.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
100
+ velbus_aio-2024.7.0.dist-info/METADATA,sha256=Y590SRjMvVtmhl4FQ5A57mqBqo-KCGF_ABRZwKYVACM,3259
101
+ velbus_aio-2024.7.0.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
102
+ velbus_aio-2024.7.0.dist-info/top_level.txt,sha256=W0-lSOwD23mm8FqaIe9vY20fKicBMIdUVjF-zmfxRnY,15
103
+ velbus_aio-2024.7.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (70.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
velbusaio/channels.py CHANGED
@@ -508,6 +508,7 @@ class Temperature(Channel):
508
508
  _min = None
509
509
  _target = 0
510
510
  _cmode = None
511
+ _coolmode = None
511
512
  _cstatus = None
512
513
  _thermostat = False
513
514
  _sleep_timer = 0
velbusaio/const.py CHANGED
@@ -38,7 +38,13 @@ RTR: Final = 0x40
38
38
  NO_RTR: Final = 0x00
39
39
 
40
40
  CACHEDIR: Final = ".velbuscache"
41
- LOAD_TIMEOUT: Final = 600
41
+
42
+ # Module scan timeout values (in mSec)
43
+ SCAN_MODULETYPE_TIMEOUT: Final = 2000 # time to wait for ModuleTypeRequest
44
+ SCAN_MODULEINFO_TIMEOUT_INITIAL: Final = 1000 # time to wait for first info (status)
45
+ SCAN_MODULEINFO_TIMEOUT_INTERVAL: Final = (
46
+ 150 # time to wait for info interval (between next message)
47
+ )
42
48
 
43
49
  DEVICE_CLASS_ILLUMINANCE: Final = "illuminance"
44
50
  DEVICE_CLASS_TEMPERATURE: Final = "temperature"
velbusaio/controller.py CHANGED
@@ -14,7 +14,6 @@ import serial
14
14
  import serial_asyncio_fast
15
15
 
16
16
  from velbusaio.channels import Channel
17
- from velbusaio.const import LOAD_TIMEOUT
18
17
  from velbusaio.exceptions import VelbusConnectionFailed
19
18
  from velbusaio.handler import PacketHandler
20
19
  from velbusaio.helpers import get_cache_dir
@@ -42,17 +41,16 @@ class Velbus:
42
41
  self._protocol = VelbusProtocol(
43
42
  message_received_callback=self._on_message_received,
44
43
  connection_lost_callback=self._on_connection_lost,
45
- end_of_scan_callback=self._on_end_of_scan,
46
44
  )
47
45
  self._closing = False
48
46
  self._auto_reconnect = True
49
47
 
50
48
  self._dsn = dsn
51
- self._handler = PacketHandler(self.send, self)
49
+ self._handler = PacketHandler(self)
52
50
  self._modules: dict[int, Module] = {}
53
51
  self._submodules: list[int] = []
54
- self._send_queue = asyncio.Queue()
55
- self._cache_dir = cache_dir
52
+ self._send_queue: asyncio.Queue = asyncio.Queue()
53
+ self._cache_dir: str = cache_dir
56
54
  # make sure the cachedir exists
57
55
  pathlib.Path(self._cache_dir).mkdir(parents=True, exist_ok=True)
58
56
 
@@ -66,11 +64,7 @@ class Velbus:
66
64
  self._log.debug("Reconnecting to transport")
67
65
  asyncio.ensure_future(self.connect())
68
66
 
69
- def _on_end_of_scan(self) -> None:
70
- """Notify the scan failure."""
71
- self._handler.scan_finished()
72
-
73
- async def add_module(
67
+ def add_module(
74
68
  self,
75
69
  addr: int,
76
70
  typ: int,
@@ -81,8 +75,7 @@ class Velbus:
81
75
  build_week: int | None = None,
82
76
  ) -> None:
83
77
  """Add a found module to the module cache."""
84
- self._log.info(f"Found module: type:{typ} address:{addr}")
85
- self._modules[addr] = Module.factory(
78
+ module = Module.factory(
86
79
  addr,
87
80
  typ,
88
81
  data,
@@ -92,30 +85,31 @@ class Velbus:
92
85
  memorymap=memorymap,
93
86
  cache_dir=self._cache_dir,
94
87
  )
95
- self._modules[addr].initialize(self.send)
96
- await self._modules[addr].load()
88
+ module.initialize(self.send)
89
+ self._modules[addr] = module
90
+ self._log.info(f"Found module {addr}: {module}")
97
91
 
98
- async def add_submodules(self, addr: int, subList: dict[int, int]) -> None:
92
+ def add_submodules(self, module: Module, subList: dict[int, int]) -> None:
99
93
  """Add submodules address to module."""
100
94
  for sub_num, sub_addr in subList.items():
101
95
  if sub_addr == 0xFF:
102
96
  continue
103
97
  self._submodules.append(sub_addr)
104
- self._modules[addr]._sub_address[sub_num] = sub_addr
105
- self._modules[sub_addr] = self._modules[addr]
106
- self._modules[addr].cleanupSubChannels()
98
+ module._sub_address[sub_num] = sub_addr
99
+ self._modules[sub_addr] = module
100
+ module.cleanupSubChannels()
107
101
 
108
102
  def get_modules(self) -> dict:
109
103
  """Return the module cache."""
110
104
  return self._modules
111
105
 
112
- def get_module(self, addr: str) -> None | Module:
106
+ def get_module(self, addr: int) -> None | Module:
113
107
  """Get a module on an address."""
114
108
  if addr in self._modules:
115
109
  return self._modules[addr]
116
110
  return None
117
111
 
118
- def get_channels(self, addr: str) -> None | dict:
112
+ def get_channels(self, addr: int) -> None | dict:
119
113
  """Get the channels for an address."""
120
114
  if addr in self._modules:
121
115
  return (self._modules[addr]).get_channels()
@@ -129,6 +123,7 @@ class Velbus:
129
123
 
130
124
  async def connect(self, test_connect: bool = False) -> None:
131
125
  """Connect to the bus and load all the data."""
126
+ await self._handler.read_protocol_data()
132
127
  auth = None
133
128
  # connect to the bus
134
129
  if ":" in self._dsn:
@@ -181,44 +176,15 @@ class Velbus:
181
176
  await self._protocol.write_auth_key(auth)
182
177
 
183
178
  # scan the bus
184
- await self.scan()
179
+ await self._handler.scan()
185
180
 
186
181
  async def scan(self) -> None:
187
- """Scan the bus."""
188
- self._handler.scan_started()
189
- for addr in range(1, 256):
190
- msg = ModuleTypeRequestMessage(addr)
191
- await self.send(msg)
192
- await self._handler._scan_complete_event.wait()
193
- # calculate how long to wait
194
- calc_timeout = len(self._modules) * 30
195
- if calc_timeout < LOAD_TIMEOUT:
196
- timeout = calc_timeout
197
- else:
198
- timeout = LOAD_TIMEOUT
199
- # create a task to wait until we have all modules loaded
200
- tsk = asyncio.Task(self._check_if_modules_are_loaded())
201
- try:
202
- await asyncio.wait_for(tsk, timeout=timeout)
203
- except asyncio.TimeoutError:
204
- self._log.error(
205
- f"Not all modules are loaded within a timeout of {LOAD_TIMEOUT} seconds, continuing with the loaded modules"
206
- )
182
+ """Service endpoint to restart the scan"""
183
+ await self._handler.scan(True)
207
184
 
208
- async def _check_if_modules_are_loaded(self) -> None:
209
- """Task to wait until modules are loaded."""
210
- while True:
211
- mods_loaded = 0
212
- for mod in (self.get_modules()).values():
213
- if mod.is_loaded():
214
- mods_loaded += 1
215
- else:
216
- self._log.warning(f"Waiting for module {mod._address}")
217
- if mods_loaded == len(self.get_modules()):
218
- self._log.info("All modules loaded")
219
- return
220
- self._log.info("Not all modules loaded yet, waiting 15 seconds")
221
- await asyncio.sleep(15)
185
+ async def sendTypeRequestMessage(self, address: int) -> None:
186
+ msg = ModuleTypeRequestMessage(address)
187
+ await self.send(msg)
222
188
 
223
189
  async def send(self, msg: Message) -> None:
224
190
  """Send a packet."""
velbusaio/handler.py CHANGED
@@ -5,9 +5,19 @@ 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
+
8
12
  import asyncio
9
13
  import json
10
14
  import logging
15
+ import threading
16
+ import os
17
+ import pathlib
18
+
19
+ from aiofile import async_open
20
+
11
21
  from typing import TYPE_CHECKING, Awaitable, Callable
12
22
  import pkg_resources
13
23
 
@@ -15,8 +25,10 @@ from velbusaio.command_registry import commandRegistry
15
25
  from velbusaio.helpers import h2, keys_exists
16
26
  from velbusaio.message import Message
17
27
  from velbusaio.messages.module_subtype import ModuleSubTypeMessage
18
- from velbusaio.messages.module_type import ModuleTypeMessage
28
+ from velbusaio.messages.module_type import ModuleTypeMessage, ModuleType2Message
19
29
  from velbusaio.raw_message import RawMessage
30
+ from velbusaio.helpers import get_cache_dir
31
+
20
32
 
21
33
  if TYPE_CHECKING:
22
34
  from velbusaio.controller import Velbus
@@ -29,27 +41,102 @@ class PacketHandler:
29
41
 
30
42
  def __init__(
31
43
  self,
32
- writer: Callable[[Message], Awaitable[None]],
33
44
  velbus: Velbus,
34
45
  ) -> None:
35
- self._log = logging.getLogger("velbus-packet")
36
- self._writer = writer
46
+ self._log = logging.getLogger("velbus-handler")
47
+ self._log.setLevel(logging.DEBUG)
37
48
  self._velbus = velbus
49
+ self._typeResponseReceived = asyncio.Event()
50
+ self._scanLock = threading.Lock()
51
+ self._modulescan_address = 0
38
52
  self._scan_complete = False
39
- self._scan_complete_event = asyncio.Event()
40
- with open(
53
+ self._scan_delay_msec = 0
54
+
55
+ async def read_protocol_data(self):
56
+ async with async_open(
41
57
  pkg_resources.resource_filename(__name__, "protocol.json")
42
58
  ) as protocol_file:
43
- self.pdata = json.load(protocol_file)
59
+ self.pdata = json.loads(await protocol_file.read())
44
60
 
45
- def scan_finished(self) -> None:
46
- self._scan_complete = True
47
- self._scan_complete_event.set()
48
- self._log.debug("Scan complete")
61
+ def empty_cache(self) -> bool:
62
+ if (
63
+ len(
64
+ [
65
+ name
66
+ for name in os.listdir(f"{get_cache_dir()}")
67
+ if os.path.isfile(f"{get_cache_dir()}/{name}")
68
+ ]
69
+ )
70
+ == 0
71
+ ):
72
+ return True
73
+ return False
49
74
 
50
- def scan_started(self) -> None:
51
- self._scan_complete = False
52
- self._scan_complete_event.clear()
75
+ async def scan(self, reload_cache: bool = False) -> None:
76
+ if reload_cache:
77
+ self._modulescan_address = 0
78
+ self._scan_complete = False
79
+ # non-blocking check to see if the cache_dir is empty
80
+ loop = asyncio.get_running_loop()
81
+ if not reload_cache and await loop.run_in_executor(None, self.empty_cache):
82
+ self._log.info("No cache yet, so forcing a bus scan")
83
+ reload_cache = True
84
+ self._log.info("Start module scan")
85
+ while self._modulescan_address < 254:
86
+ address = 0
87
+ module = None
88
+ with self._scanLock:
89
+ self._modulescan_address = self._modulescan_address + 1
90
+ address = self._modulescan_address
91
+ module = self._velbus.get_module(address)
92
+
93
+ self._log.info(f"Starting handling scan {address}")
94
+
95
+ cfile = pathlib.Path(f"{get_cache_dir()}/{address}.json")
96
+ # cleanup the old module cache if needed
97
+ scanModule = reload_cache
98
+ if scanModule and os.path.isfile(cfile):
99
+ os.remove(cfile)
100
+ elif os.path.isfile(cfile):
101
+ scanModule = os.path.isfile(cfile)
102
+ if scanModule:
103
+ try:
104
+ self._log.info(f"Starting scan {address}")
105
+ self._typeResponseReceived.clear()
106
+ await self._velbus.sendTypeRequestMessage(address)
107
+ await asyncio.wait_for(
108
+ self._typeResponseReceived.wait(),
109
+ SCAN_MODULETYPE_TIMEOUT / 1000.0,
110
+ )
111
+ with self._scanLock:
112
+ module = self._velbus.get_module(address)
113
+ except asyncio.TimeoutError:
114
+ self._log.info(
115
+ f"Scan module {address} failed: not present or unavailable"
116
+ )
117
+ if module is not None:
118
+ try:
119
+ self._log.debug(f"Module {address} detected: start loading")
120
+ await asyncio.wait_for(
121
+ module.load(from_cache=True),
122
+ SCAN_MODULEINFO_TIMEOUT_INITIAL / 1000.0,
123
+ )
124
+ self._scan_delay_msec = SCAN_MODULEINFO_TIMEOUT_INITIAL
125
+ while self._scan_delay_msec > 50 and not module.is_loaded():
126
+ # self._log.debug(
127
+ # f"\t... waiting {self._scan_delay_msec} is_loaded={module.is_loaded()}"
128
+ # )
129
+ self._scan_delay_msec = self._scan_delay_msec - 50
130
+ await asyncio.sleep(0.05)
131
+ self._log.info(
132
+ f"Scan module {address} completed, module loaded={module.is_loaded()}"
133
+ )
134
+ except asyncio.TimeoutError:
135
+ self._log.error(
136
+ f"Module {address} did not respond to info requests after successful type request"
137
+ )
138
+ self._scan_complete = True
139
+ self._log.info("Module scan completed")
53
140
 
54
141
  async def handle(self, rawmsg: RawMessage) -> None:
55
142
  """
@@ -66,98 +153,124 @@ class PacketHandler:
66
153
  command_value = rawmsg.command
67
154
  data = rawmsg.data_only
68
155
 
69
- if command_value == 0xFF and not self._scan_complete:
70
- msg = ModuleTypeMessage()
71
- msg.populate(priority, address, rtr, data)
72
- self._log.debug(f"Received {msg}")
73
- await self._handle_module_type(msg)
74
- elif command_value in (0xB0, 0xA7, 0xA6) and not self._scan_complete:
75
- msg = ModuleSubTypeMessage()
76
- msg.populate(priority, address, rtr, data)
77
-
78
- if command_value == 0xB0:
79
- msg.sub_address_offset = 0
80
- elif command_value == 0xA7:
81
- msg.sub_address_offset = 4
82
- elif command_value == 0xA6:
83
- msg.sub_address_offset = 8
84
- else:
85
- raise RuntimeError("Unreachable code reached => bug here")
156
+ # handle module type response message
157
+ if command_value == 0xFF:
158
+ if not self._scan_complete:
159
+ tmsg: ModuleTypeMessage = ModuleTypeMessage()
160
+ tmsg.populate(priority, address, rtr, data)
161
+ with self._scanLock:
162
+ self._handle_module_type(tmsg)
163
+ if address == self._modulescan_address:
164
+ self._typeResponseReceived.set()
165
+ else:
166
+ self._log.debug(
167
+ f"Unexpected module type message module address {address}, Velbuslink scan?"
168
+ )
169
+ self._modulescan_address = address - 1
170
+
171
+ self._typeResponseReceived.set()
172
+
173
+ # handle module subtype response message
174
+ elif command_value in (0xB0, 0xA7, 0xA6):
175
+ if not self._scan_complete:
176
+ msg: ModuleSubTypeMessage = ModuleSubTypeMessage()
177
+ msg.populate(priority, address, rtr, data)
178
+ if command_value == 0xB0:
179
+ msg.sub_address_offset = 0
180
+ elif command_value == 0xA7:
181
+ msg.sub_address_offset = 4
182
+ elif command_value == 0xA6:
183
+ msg.sub_address_offset = 8
184
+ with self._scanLock:
185
+ self._scan_delay_msec = SCAN_MODULEINFO_TIMEOUT_INITIAL
186
+ self._handle_module_subtype(msg)
86
187
 
87
- self._log.debug(f"Received {msg}")
88
- await self._handle_module_subtype(msg)
188
+ # ignore broadcast
89
189
  elif command_value in self.pdata["MessagesBroadCast"]:
90
190
  self._log.debug(
91
191
  "Received broadcast message {} from {}, ignoring".format(
92
- self.pdata["MessageBroadCast"][command_value.upper()], address
93
- )
94
- )
95
- elif address in self._velbus.get_modules().keys():
96
- module_type = self._velbus.get_module(address).get_type()
97
- if commandRegistry.has_command(int(command_value), module_type):
98
- command = commandRegistry.get_command(command_value, module_type)
99
- msg = command()
100
- msg.populate(priority, address, rtr, data)
101
- self._log.debug(f"Received {msg}")
102
- # send the message to the modules
103
- await self._velbus.get_module(msg.address).on_message(msg)
104
- else:
105
- self._log.warning(
106
- "NOT FOUND IN command_registry: addr={} cmd={} packet={}".format(
107
- address,
108
- command_value,
109
- ":".join(format(x, "02x") for x in data),
110
- )
111
- )
112
- elif self._scan_complete:
113
- # this should only happen once the scan is complete, of its not complete suspended the error message
114
- self._log.warning(
115
- "UNKNOWN module, you should initialize a full new velbus scan: packet={}, address={}, modules={}".format(
116
- ":".join(format(x, "02x") for x in data),
117
- address,
118
- self._velbus.get_modules().keys(),
192
+ self.pdata["MessageBroadCast"][str(command_value).upper()], address
119
193
  )
120
194
  )
121
195
 
122
- async def _handle_module_type(self, msg: Message) -> None:
196
+ # handle other messages for modules that are already scanned
197
+ else:
198
+ module = None
199
+ with self._scanLock:
200
+ module = self._velbus.get_module(address)
201
+ if module is not None:
202
+ module_type = module.get_type()
203
+ if commandRegistry.has_command(int(command_value), module_type):
204
+ command = commandRegistry.get_command(command_value, module_type)
205
+ if not command:
206
+ return
207
+ msg = command()
208
+ msg.populate(priority, address, rtr, data)
209
+ # restart the info completion time when info message received
210
+ if command_value in (
211
+ 0xF0,
212
+ 0xF1,
213
+ 0xF2,
214
+ 0xFB,
215
+ 0xFE,
216
+ 0xCC,
217
+ ): # names, memory data, memory block
218
+ self._scan_delay_msec = SCAN_MODULEINFO_TIMEOUT_INTERVAL
219
+ # self._log.debug(f"Restart timeout {msg}")
220
+ # send the message to the modules
221
+ await module.on_message(msg)
222
+ else:
223
+ self._log.warning(f"NOT FOUND IN command_registry: {rawmsg}")
224
+
225
+ def _handle_module_type(self, msg: ModuleTypeMessage | ModuleType2Message) -> None:
123
226
  """
124
227
  load the module data
125
228
  """
126
- data = keys_exists(self.pdata, "ModuleTypes", h2(msg.module_type))
127
- if not data:
128
- self._log.warning(f"Module not recognized: {msg.module_type}")
129
- return
130
- # create the module
131
- await self._velbus.add_module(
132
- msg.address,
133
- msg.module_type,
134
- data,
135
- memorymap=msg.memory_map_version,
136
- build_year=msg.build_year,
137
- build_week=msg.build_week,
138
- serial=msg.serial,
139
- )
140
-
141
- async def _handle_module_subtype(self, msg: Message) -> None:
142
- if msg.address not in self._velbus.get_modules():
143
- return
144
- addrList = {
145
- (msg.sub_address_offset + 1): msg.sub_address_1,
146
- (msg.sub_address_offset + 2): msg.sub_address_2,
147
- (msg.sub_address_offset + 3): msg.sub_address_3,
148
- (msg.sub_address_offset + 4): msg.sub_address_4,
149
- }
150
- await self._velbus.add_submodules(msg.address, addrList)
151
-
152
- def _channel_convert(self, module: str, channel: str, ctype: str) -> None | int:
153
- data = keys_exists(
154
- self.pdata, "ModuleTypes", h2(module), "ChannelNumbers", ctype
155
- )
156
- if data and "Map" in data and h2(channel) in data["Map"]:
157
- return data["Map"][h2(channel)]
158
- if data and "Convert" in data:
159
- return int(channel)
160
- for offset in range(0, 8):
161
- if channel & (1 << offset):
162
- return offset + 1
163
- return None
229
+ if msg is not None:
230
+ module = self._velbus.get_module(msg.address)
231
+ if module is None:
232
+ data = keys_exists(self.pdata, "ModuleTypes", h2(msg.module_type))
233
+ if not data:
234
+ self._log.warning(f"Module not recognized: {msg.module_type}")
235
+ return
236
+ self._velbus.add_module(
237
+ msg.address,
238
+ msg.module_type,
239
+ data,
240
+ memorymap=msg.memory_map_version,
241
+ build_year=msg.build_year,
242
+ build_week=msg.build_week,
243
+ serial=msg.serial,
244
+ )
245
+ else:
246
+ self._log.debug(
247
+ f"***Module already exists scanAddr={self._modulescan_address} addr={msg.address} {msg}"
248
+ )
249
+
250
+ # else:
251
+ # self._log.debug("*** handle_module_type called without response message")
252
+
253
+ def _handle_module_subtype(self, msg: ModuleSubTypeMessage) -> None:
254
+ module = self._velbus.get_module(msg.address)
255
+ if module is not None:
256
+ addrList = {
257
+ (msg.sub_address_offset + 1): msg.sub_address_1,
258
+ (msg.sub_address_offset + 2): msg.sub_address_2,
259
+ (msg.sub_address_offset + 3): msg.sub_address_3,
260
+ (msg.sub_address_offset + 4): msg.sub_address_4,
261
+ }
262
+ self._velbus.add_submodules(module, addrList)
263
+
264
+
265
+ # def _channel_convert(self, module: str, channel: str, ctype: str) -> None | int:
266
+ # data = keys_exists(
267
+ # self.pdata, "ModuleTypes", h2(module), "ChannelNumbers", ctype
268
+ # )
269
+ # if data and "Map" in data and h2(channel) in data["Map"]:
270
+ # return data["Map"][h2(channel)]
271
+ # if data and "Convert" in data:
272
+ # return int(channel)
273
+ # for offset in range(0, 8):
274
+ # if channel & (1 << offset):
275
+ # return offset + 1
276
+ # return None
velbusaio/message.py CHANGED
@@ -20,19 +20,19 @@ class Message:
20
20
  Base Velbus message
21
21
  """
22
22
 
23
- def __init__(self, address: int = None) -> None:
23
+ def __init__(self, address: int = 0) -> None:
24
24
  self.priority = PRIORITY_LOW
25
- self.address = None
26
- self.rtr = False
25
+ self.address: int = 0
26
+ self.rtr: bool = False
27
27
  self.data = bytearray()
28
28
  self.set_defaults(address)
29
29
 
30
- def set_attributes(self, priority: int, address: int, rtr: int) -> None:
30
+ def set_attributes(self, priority: int, address: int, rtr: bool) -> None:
31
31
  self.priority = priority
32
32
  self.address = address
33
33
  self.rtr = rtr
34
34
 
35
- def populate(self, priority: int, address: int, rtr: int, data: int) -> None:
35
+ def populate(self, priority: int, address: int, rtr: bool, data: int) -> None:
36
36
  raise NotImplementedError
37
37
 
38
38
  def set_defaults(self, address: int | None) -> None:
@@ -66,7 +66,7 @@ class Message:
66
66
  if callable(getattr(self, key)) or key.startswith("__"):
67
67
  del me[key]
68
68
  if isinstance(me[key], (bytes, bytearray)):
69
- me[key] = str(me[key], "utf-8")
69
+ me[key] = str(me[key])
70
70
  return me
71
71
 
72
72
  def to_json(self) -> str:
@@ -25,7 +25,7 @@ class ModuleSubTypeMessage(Message):
25
25
 
26
26
  # pylint: disable-msg=R0902
27
27
 
28
- def __init__(self, address=None, sub_address_offset: int = 0):
28
+ def __init__(self, address=None, sub_address_offset: int = 0) -> None:
29
29
  Message.__init__(self)
30
30
  self.module_type = 0x00
31
31
  self.sub_address_1 = 0xFF
@@ -36,13 +36,13 @@ class ModuleSubTypeMessage(Message):
36
36
  self.serial = 0
37
37
  self.sub_address_offset = sub_address_offset
38
38
 
39
- def module_name(self):
39
+ def module_name(self) -> str:
40
40
  """
41
41
  :return: str
42
42
  """
43
43
  return "Unknown"
44
44
 
45
- def populate(self, priority, address, rtr, data):
45
+ def populate(self, priority, address, rtr, data) -> None:
46
46
  """
47
47
  :return: None
48
48
  """
@@ -98,7 +98,7 @@ class ModuleTypeMessage(Message):
98
98
 
99
99
  # pylint: disable-msg=R0902
100
100
 
101
- def __init__(self, address=None):
101
+ def __init__(self, address=None) -> None:
102
102
  Message.__init__(self)
103
103
  self.module_type = 0x00
104
104
  self.led_on = []
@@ -110,13 +110,13 @@ class ModuleTypeMessage(Message):
110
110
  self.build_week = 0
111
111
  self.set_defaults(address)
112
112
 
113
- def module_name(self):
113
+ def module_name(self) -> str:
114
114
  """
115
115
  :return: str
116
116
  """
117
117
  return "Unknown"
118
118
 
119
- def populate(self, priority, address, rtr, data):
119
+ def populate(self, priority, address, rtr, data) -> None:
120
120
  """
121
121
  :return: None
122
122
  """
@@ -151,7 +151,7 @@ class ModuleTypeMessage(Message):
151
151
  ],
152
152
  )
153
153
  class ModuleType2Message(Message):
154
- def __init__(self, address=None):
154
+ def __init__(self, address=None) -> None:
155
155
  Message.__init__(self)
156
156
  self.module_type = 0x00
157
157
  self.led_on = []
@@ -164,7 +164,7 @@ class ModuleType2Message(Message):
164
164
  self.term = 0
165
165
  self.set_defaults(address)
166
166
 
167
- def module_name(self):
167
+ def module_name(self) -> str:
168
168
  """
169
169
  :return: str
170
170
  """
velbusaio/module.py CHANGED
@@ -9,6 +9,7 @@ import pathlib
9
9
  import struct
10
10
  import sys
11
11
  import json
12
+ import os
12
13
  from typing import Awaitable, Callable
13
14
 
14
15
  from velbusaio.channels import (
@@ -252,6 +253,7 @@ class Module:
252
253
  """
253
254
  Process received message
254
255
  """
256
+ self._log.debug(f"RX: {message}")
255
257
  _channel_offset = self.calc_channel_offset(message.address)
256
258
 
257
259
  if isinstance(
@@ -515,7 +517,7 @@ class Module:
515
517
  try:
516
518
  await self._channels[channel].update(updates)
517
519
  except KeyError:
518
- self._log.error(
520
+ self._log.info(
519
521
  f"channel {channel} does not exist for module @ address {self}"
520
522
  )
521
523
 
@@ -526,18 +528,6 @@ class Module:
526
528
  return self._channels
527
529
 
528
530
  async def load(self, from_cache: bool = False) -> None:
529
- """
530
- Retrieve names of channels
531
- """
532
- # did we already start the loading?
533
- # this is needed for the submodules,
534
- # as the submodule address maps to the main module
535
- # this method can be called multiple times
536
- if self._is_loading or self.loaded:
537
- if from_cache:
538
- await self._request_module_status()
539
- return
540
- self._log.info("Load Module")
541
531
  # start the loading
542
532
  self._is_loading = True
543
533
  # see if we have a cache
@@ -548,14 +538,15 @@ class Module:
548
538
  except OSError:
549
539
  cache = {}
550
540
  # load default channels
551
- await self._load_default_channels()
541
+ await self.__load_default_channels()
542
+
552
543
  # load the data from memory ( the stuff that we need)
553
544
  if "name" in cache and cache["name"] != "":
554
545
  self._name = cache["name"]
555
546
  else:
556
547
  await self.__load_memory()
557
548
  # load the module status
558
- await self._request_module_status()
549
+ # await self._request_module_status()
559
550
  # load the channel names
560
551
  if "channels" in cache:
561
552
  for num, chan in cache["channels"].items():
@@ -569,6 +560,7 @@ class Module:
569
560
  self._load()
570
561
  # stop the loading
571
562
  self._is_loading = False
563
+ await self._request_module_status()
572
564
 
573
565
  def _load(self) -> None:
574
566
  """
@@ -674,6 +666,8 @@ class Module:
674
666
  if "Channels" not in self._data:
675
667
  # some modules have no channels
676
668
  return
669
+ self._log.info(f"Request module status {self._address}")
670
+
677
671
  mod_stat_req_msg = ModuleStatusRequestMessage(self._address)
678
672
  counter_msg = None
679
673
  for chan, chan_data in self._data["Channels"].items():
@@ -728,7 +722,7 @@ class Module:
728
722
  msg.low_address = addr[1]
729
723
  await self._writer(msg)
730
724
 
731
- async def _load_default_channels(self) -> None:
725
+ async def __load_default_channels(self) -> None:
732
726
  if "Channels" not in self._data:
733
727
  return
734
728
 
@@ -777,6 +771,7 @@ class VmbDali(Module):
777
771
  self.group_members: dict[int, set[int]] = {}
778
772
 
779
773
  async def _load_default_channels(self) -> None:
774
+ await super().load()
780
775
  for chan in range(1, 64 + 1):
781
776
  self._channels[chan] = Channel(
782
777
  self, chan, "placeholder", True, self._writer, self._address
velbusaio/protocol.json CHANGED
@@ -9045,6 +9045,74 @@
9045
9045
  "Type": "SelectedProgram"
9046
9046
  }
9047
9047
  },
9048
+ "Memory": {
9049
+ "Address": {
9050
+ "049C": { "ModuleName": "0:Start" },
9051
+ "049D": { "ModuleName": "1" },
9052
+ "049E": { "ModuleName": "2" },
9053
+ "049F": { "ModuleName": "3" },
9054
+ "04A0": { "ModuleName": "4" },
9055
+ "04A1": { "ModuleName": "5" },
9056
+ "04A2": { "ModuleName": "6" },
9057
+ "04A3": { "ModuleName": "7" },
9058
+ "04A4": { "ModuleName": "8" },
9059
+ "04A5": { "ModuleName": "9" },
9060
+ "04A6": { "ModuleName": "10" },
9061
+ "04A7": { "ModuleName": "11" },
9062
+ "04A8": { "ModuleName": "12" },
9063
+ "04A9": { "ModuleName": "13" },
9064
+ "04AA": { "ModuleName": "14" },
9065
+ "04AB": { "ModuleName": "15" },
9066
+ "04AC": { "ModuleName": "16" },
9067
+ "04AD": { "ModuleName": "17" },
9068
+ "04AE": { "ModuleName": "18" },
9069
+ "04AF": { "ModuleName": "19" },
9070
+ "04B0": { "ModuleName": "20" },
9071
+ "04B1": { "ModuleName": "21" },
9072
+ "04B2": { "ModuleName": "22" },
9073
+ "04B3": { "ModuleName": "23" },
9074
+ "04B4": { "ModuleName": "24" },
9075
+ "04B5": { "ModuleName": "25" },
9076
+ "04B6": { "ModuleName": "26" },
9077
+ "04B7": { "ModuleName": "27" },
9078
+ "04B8": { "ModuleName": "28" },
9079
+ "04B9": { "ModuleName": "29" },
9080
+ "04BA": { "ModuleName": "30" },
9081
+ "04BB": { "ModuleName": "31" },
9082
+ "04BC": { "ModuleName": "32" },
9083
+ "04BD": { "ModuleName": "33" },
9084
+ "04BE": { "ModuleName": "34" },
9085
+ "04BF": { "ModuleName": "35" },
9086
+ "04C0": { "ModuleName": "36" },
9087
+ "04C1": { "ModuleName": "37" },
9088
+ "04C2": { "ModuleName": "38" },
9089
+ "04C3": { "ModuleName": "39" },
9090
+ "04C4": { "ModuleName": "40" },
9091
+ "04C5": { "ModuleName": "41" },
9092
+ "04C6": { "ModuleName": "42" },
9093
+ "04C7": { "ModuleName": "43" },
9094
+ "04C8": { "ModuleName": "44" },
9095
+ "04C9": { "ModuleName": "45" },
9096
+ "04CA": { "ModuleName": "46" },
9097
+ "04CB": { "ModuleName": "47" },
9098
+ "04CC": { "ModuleName": "48" },
9099
+ "04CD": { "ModuleName": "49" },
9100
+ "04CE": { "ModuleName": "50" },
9101
+ "04CF": { "ModuleName": "51" },
9102
+ "04D0": { "ModuleName": "52" },
9103
+ "04D1": { "ModuleName": "53" },
9104
+ "04D2": { "ModuleName": "54" },
9105
+ "04D3": { "ModuleName": "55" },
9106
+ "04D4": { "ModuleName": "56" },
9107
+ "04D5": { "ModuleName": "57" },
9108
+ "04D6": { "ModuleName": "58" },
9109
+ "04D7": { "ModuleName": "59" },
9110
+ "04D8": { "ModuleName": "60" },
9111
+ "04D9": { "ModuleName": "61" },
9112
+ "04DA": { "ModuleName": "62" },
9113
+ "04DB": { "ModuleName": "64:Save" }
9114
+ }
9115
+ },
9048
9116
  "Info": "8 channel 0 to 10 V dimmer control module",
9049
9117
  "Type": "VMB8DC-20"
9050
9118
  },
velbusaio/protocol.py CHANGED
@@ -15,9 +15,7 @@ from velbusaio.raw_message import create as create_message_info
15
15
 
16
16
  def _on_write_backoff(details):
17
17
  logging.debug(
18
- "Transport is not open, waiting {wait} seconds after {tries}",
19
- wait=details.wait,
20
- tries=details.tries,
18
+ f"Transport is not open, waiting {details.wait} seconds after {details.tries}"
21
19
  )
22
20
 
23
21
 
@@ -31,13 +29,11 @@ class VelbusProtocol(asyncio.BufferedProtocol):
31
29
  self,
32
30
  message_received_callback: t.Callable[[RawMessage], t.Awaitable[None]],
33
31
  connection_lost_callback=None,
34
- end_of_scan_callback=None,
35
32
  ) -> None:
36
33
  super().__init__()
37
34
  self._log = logging.getLogger("velbus-protocol")
38
35
  self._message_received_callback = message_received_callback
39
36
  self._connection_lost_callback = connection_lost_callback
40
- self._end_of_scan_callback = end_of_scan_callback
41
37
 
42
38
  # everything for reading from Velbus
43
39
  self._buffer = bytearray(MAXIMUM_MESSAGE_SIZE)
@@ -63,14 +59,14 @@ class VelbusProtocol(asyncio.BufferedProtocol):
63
59
  self._restart_writer = True
64
60
  self.restart_writing()
65
61
 
66
- async def pause_writing(self):
62
+ async def pause_writing(self) -> None:
67
63
  """Pause writing."""
68
64
  self._restart_writer = False
69
65
  if self._writer_task:
70
66
  self._send_queue.put_nowait(None)
71
67
  await asyncio.sleep(0.1)
72
68
 
73
- def restart_writing(self):
69
+ def restart_writing(self) -> None:
74
70
  """Resume writing."""
75
71
  if self._restart_writer and not self._write_transport_lock.locked():
76
72
  self._writer_task = asyncio.ensure_future(
@@ -78,7 +74,7 @@ class VelbusProtocol(asyncio.BufferedProtocol):
78
74
  )
79
75
  self._writer_task.add_done_callback(lambda _future: self.restart_writing())
80
76
 
81
- def close(self):
77
+ def close(self) -> None:
82
78
  self._closing = True
83
79
  self._restart_writer = False
84
80
  if self.transport:
@@ -197,8 +193,6 @@ class VelbusProtocol(asyncio.BufferedProtocol):
197
193
  queue_sleep_time = SLEEP_TIME * 33
198
194
  else:
199
195
  queue_sleep_time = SLEEP_TIME
200
- if msg_info.rtr and msg_info.address == 0xFF:
201
- self._end_of_scan_callback()
202
196
  await asyncio.sleep(queue_sleep_time)
203
197
  except (asyncio.CancelledError, GeneratorExit) as exc:
204
198
  if not self._closing: