velbus-aio 2024.4.1__tar.gz → 2024.7.0__tar.gz
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.
- {velbus-aio-2024.4.1/velbus_aio.egg-info → velbus_aio-2024.7.0}/PKG-INFO +2 -2
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/pyproject.toml +3 -3
- velbus_aio-2024.7.0/requirements.txt +5 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0/velbus_aio.egg-info}/PKG-INFO +2 -2
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbus_aio.egg-info/requires.txt +1 -1
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/channels.py +5 -1
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/const.py +7 -1
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/controller.py +34 -66
- velbus_aio-2024.7.0/velbusaio/handler.py +276 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/message.py +6 -6
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/module_subtype.py +3 -3
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/module_type.py +5 -5
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/set_dimmer.py +7 -12
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/module.py +20 -27
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/protocol.json +68 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/protocol.py +4 -10
- velbus-aio-2024.4.1/requirements.txt +0 -3
- velbus-aio-2024.4.1/velbusaio/handler.py +0 -163
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/LICENSE +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/MANIFEST.in +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/README.md +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/setup.cfg +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/setup.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbus_aio.egg-info/SOURCES.txt +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbus_aio.egg-info/dependency_links.txt +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbus_aio.egg-info/not-zip-safe +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbus_aio.egg-info/top_level.txt +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/__init__.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/command_registry.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/discovery.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/exceptions.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/helpers.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/__init__.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/blind_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/bus_active.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/bus_error_counter_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/bus_error_counter_status_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/bus_off.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/channel_name_part1.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/channel_name_part2.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/channel_name_part3.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/channel_name_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/clear_led.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/counter_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/counter_status_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/cover_down.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/cover_off.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/cover_position.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/cover_up.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/dali_device_settings.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/dali_device_settings_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/dali_dim_value_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/dimmer_channel_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/dimmer_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/edge_set_color.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/edge_set_custom_color.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/fast_blinking_led.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/forced_off.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/forced_on.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/interface_status_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/ir_receiver_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/kwh_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/light_value_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/memo_text.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/memory_data.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/memory_data_block.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/memory_dump_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/module_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/module_status_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/module_type_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/push_button_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/raw.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/read_data_block_from_memory.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/read_data_from_memory.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/realtime_clock_status_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/receive_buffer_full.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/receive_ready.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/relay_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/restore_dimmer.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/select_program.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/sensor_settings_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/sensor_temp_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/sensor_temperature.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/set_date.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/set_daylight_saving.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/set_led.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/set_realtime_clock.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/set_temperature.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/slider_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/slow_blinking_led.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/start_relay_blinking_timer.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/start_relay_timer.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/switch_relay_off.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/switch_relay_on.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/switch_to_comfort.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/switch_to_day.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/switch_to_night.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/switch_to_safe.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/temp_sensor_settings_part1.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/temp_sensor_settings_part2.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/temp_sensor_settings_part3.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/temp_sensor_settings_part4.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/temp_sensor_settings_request.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/temp_sensor_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/temp_set_cooling.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/temp_set_heating.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/update_led_status.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/very_fast_blinking_led.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/write_data_to_memory.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/write_memory_block.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/messages/write_module_address_and_serial_number.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/py.typed +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/raw_message.py +0 -0
- {velbus-aio-2024.4.1 → velbus_aio-2024.7.0}/velbusaio/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: velbus-aio
|
|
3
|
-
Version: 2024.
|
|
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
|
|
@@ -26,7 +26,7 @@ Requires-Python: >=3.8.0
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: pyserial>=3.5.0
|
|
29
|
-
Requires-Dist: pyserial-
|
|
29
|
+
Requires-Dist: pyserial-asyncio_fast>=0.11
|
|
30
30
|
Requires-Dist: backoff>=1.10.0
|
|
31
31
|
|
|
32
32
|

|
|
@@ -4,7 +4,7 @@ requires = ["setuptools", "wheel"]
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "velbus-aio"
|
|
6
6
|
license = {text = "MIT"}
|
|
7
|
-
version = "2024.
|
|
7
|
+
version = "2024.7.0"
|
|
8
8
|
description = "Open-source home automation platform running on Python 3."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -30,7 +30,7 @@ classifiers = [
|
|
|
30
30
|
requires-python = ">=3.8.0"
|
|
31
31
|
dependencies = [
|
|
32
32
|
"pyserial>=3.5.0",
|
|
33
|
-
"pyserial-
|
|
33
|
+
"pyserial-asyncio_fast>=0.11",
|
|
34
34
|
"backoff>=1.10.0",
|
|
35
35
|
]
|
|
36
36
|
|
|
@@ -51,7 +51,7 @@ exclude_dirs = ["tests"]
|
|
|
51
51
|
skips = ["B301", "B403", "B323", "B104", "B110"]
|
|
52
52
|
|
|
53
53
|
[tool.bumpver]
|
|
54
|
-
current_version = "2024.
|
|
54
|
+
current_version = "2024.7.0"
|
|
55
55
|
version_pattern = "YYYY.MM.INC0"
|
|
56
56
|
commit_message = "bump version {old_version} -> {new_version}"
|
|
57
57
|
commit = true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: velbus-aio
|
|
3
|
-
Version: 2024.
|
|
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
|
|
@@ -26,7 +26,7 @@ Requires-Python: >=3.8.0
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: pyserial>=3.5.0
|
|
29
|
-
Requires-Dist: pyserial-
|
|
29
|
+
Requires-Dist: pyserial-asyncio_fast>=0.11
|
|
30
30
|
Requires-Dist: backoff>=1.10.0
|
|
31
31
|
|
|
32
32
|

|
|
@@ -137,7 +137,10 @@ class Channel:
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
def to_cache(self) -> dict:
|
|
140
|
-
|
|
140
|
+
dst = {"name": self._name, "type": type(self).__name__}
|
|
141
|
+
if hasattr(self, "_Unit"):
|
|
142
|
+
dst["Unit"] = self._Unit
|
|
143
|
+
return dst
|
|
141
144
|
|
|
142
145
|
def __setstate__(self, state):
|
|
143
146
|
self.__dict__.update(state)
|
|
@@ -505,6 +508,7 @@ class Temperature(Channel):
|
|
|
505
508
|
_min = None
|
|
506
509
|
_target = 0
|
|
507
510
|
_cmode = None
|
|
511
|
+
_coolmode = None
|
|
508
512
|
_cstatus = None
|
|
509
513
|
_thermostat = False
|
|
510
514
|
_sleep_timer = 0
|
|
@@ -38,7 +38,13 @@ RTR: Final = 0x40
|
|
|
38
38
|
NO_RTR: Final = 0x00
|
|
39
39
|
|
|
40
40
|
CACHEDIR: Final = ".velbuscache"
|
|
41
|
-
|
|
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"
|
|
@@ -11,10 +11,9 @@ import time
|
|
|
11
11
|
from urllib.parse import urlparse
|
|
12
12
|
|
|
13
13
|
import serial
|
|
14
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
88
|
+
module.initialize(self.send)
|
|
89
|
+
self._modules[addr] = module
|
|
90
|
+
self._log.info(f"Found module {addr}: {module}")
|
|
97
91
|
|
|
98
|
-
|
|
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
|
-
|
|
105
|
-
self._modules[sub_addr] =
|
|
106
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
@@ -159,16 +154,18 @@ class Velbus:
|
|
|
159
154
|
else:
|
|
160
155
|
# serial port
|
|
161
156
|
try:
|
|
162
|
-
_transport, _protocol =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
157
|
+
_transport, _protocol = (
|
|
158
|
+
await serial_asyncio_fast.create_serial_connection(
|
|
159
|
+
asyncio.get_event_loop(),
|
|
160
|
+
lambda: self._protocol,
|
|
161
|
+
url=self._dsn,
|
|
162
|
+
baudrate=38400,
|
|
163
|
+
bytesize=serial.EIGHTBITS,
|
|
164
|
+
parity=serial.PARITY_NONE,
|
|
165
|
+
stopbits=serial.STOPBITS_ONE,
|
|
166
|
+
xonxoff=0,
|
|
167
|
+
rtscts=1,
|
|
168
|
+
)
|
|
172
169
|
)
|
|
173
170
|
except (FileNotFoundError, serial.SerialException) as err:
|
|
174
171
|
raise VelbusConnectionFailed from err
|
|
@@ -179,44 +176,15 @@ class Velbus:
|
|
|
179
176
|
await self._protocol.write_auth_key(auth)
|
|
180
177
|
|
|
181
178
|
# scan the bus
|
|
182
|
-
await self.scan()
|
|
179
|
+
await self._handler.scan()
|
|
183
180
|
|
|
184
181
|
async def scan(self) -> None:
|
|
185
|
-
"""
|
|
186
|
-
self._handler.
|
|
187
|
-
for addr in range(1, 256):
|
|
188
|
-
msg = ModuleTypeRequestMessage(addr)
|
|
189
|
-
await self.send(msg)
|
|
190
|
-
await self._handler._scan_complete_event.wait()
|
|
191
|
-
# calculate how long to wait
|
|
192
|
-
calc_timeout = len(self._modules) * 30
|
|
193
|
-
if calc_timeout < LOAD_TIMEOUT:
|
|
194
|
-
timeout = calc_timeout
|
|
195
|
-
else:
|
|
196
|
-
timeout = LOAD_TIMEOUT
|
|
197
|
-
# create a task to wait until we have all modules loaded
|
|
198
|
-
tsk = asyncio.Task(self._check_if_modules_are_loaded())
|
|
199
|
-
try:
|
|
200
|
-
await asyncio.wait_for(tsk, timeout=timeout)
|
|
201
|
-
except asyncio.TimeoutError:
|
|
202
|
-
self._log.error(
|
|
203
|
-
f"Not all modules are loaded within a timeout of {LOAD_TIMEOUT} seconds, continuing with the loaded modules"
|
|
204
|
-
)
|
|
182
|
+
"""Service endpoint to restart the scan"""
|
|
183
|
+
await self._handler.scan(True)
|
|
205
184
|
|
|
206
|
-
async def
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
mods_loaded = 0
|
|
210
|
-
for mod in (self.get_modules()).values():
|
|
211
|
-
if mod.is_loaded():
|
|
212
|
-
mods_loaded += 1
|
|
213
|
-
else:
|
|
214
|
-
self._log.warning(f"Waiting for module {mod._address}")
|
|
215
|
-
if mods_loaded == len(self.get_modules()):
|
|
216
|
-
self._log.info("All modules loaded")
|
|
217
|
-
return
|
|
218
|
-
self._log.info("Not all modules loaded yet, waiting 15 seconds")
|
|
219
|
-
await asyncio.sleep(15)
|
|
185
|
+
async def sendTypeRequestMessage(self, address: int) -> None:
|
|
186
|
+
msg = ModuleTypeRequestMessage(address)
|
|
187
|
+
await self.send(msg)
|
|
220
188
|
|
|
221
189
|
async def send(self, msg: Message) -> None:
|
|
222
190
|
"""Send a packet."""
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Velbus packet handler
|
|
3
|
+
:Author maikel punie <maikel.punie@gmail.com>
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from velbusaio.const import SCAN_MODULETYPE_TIMEOUT
|
|
9
|
+
from velbusaio.const import SCAN_MODULEINFO_TIMEOUT_INITIAL
|
|
10
|
+
from velbusaio.const import SCAN_MODULEINFO_TIMEOUT_INTERVAL
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import threading
|
|
16
|
+
import os
|
|
17
|
+
import pathlib
|
|
18
|
+
|
|
19
|
+
from aiofile import async_open
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING, Awaitable, Callable
|
|
22
|
+
import pkg_resources
|
|
23
|
+
|
|
24
|
+
from velbusaio.command_registry import commandRegistry
|
|
25
|
+
from velbusaio.helpers import h2, keys_exists
|
|
26
|
+
from velbusaio.message import Message
|
|
27
|
+
from velbusaio.messages.module_subtype import ModuleSubTypeMessage
|
|
28
|
+
from velbusaio.messages.module_type import ModuleTypeMessage, ModuleType2Message
|
|
29
|
+
from velbusaio.raw_message import RawMessage
|
|
30
|
+
from velbusaio.helpers import get_cache_dir
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from velbusaio.controller import Velbus
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PacketHandler:
|
|
38
|
+
"""
|
|
39
|
+
The packetHandler class
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
velbus: Velbus,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._log = logging.getLogger("velbus-handler")
|
|
47
|
+
self._log.setLevel(logging.DEBUG)
|
|
48
|
+
self._velbus = velbus
|
|
49
|
+
self._typeResponseReceived = asyncio.Event()
|
|
50
|
+
self._scanLock = threading.Lock()
|
|
51
|
+
self._modulescan_address = 0
|
|
52
|
+
self._scan_complete = False
|
|
53
|
+
self._scan_delay_msec = 0
|
|
54
|
+
|
|
55
|
+
async def read_protocol_data(self):
|
|
56
|
+
async with async_open(
|
|
57
|
+
pkg_resources.resource_filename(__name__, "protocol.json")
|
|
58
|
+
) as protocol_file:
|
|
59
|
+
self.pdata = json.loads(await protocol_file.read())
|
|
60
|
+
|
|
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
|
|
74
|
+
|
|
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")
|
|
140
|
+
|
|
141
|
+
async def handle(self, rawmsg: RawMessage) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Handle a received packet
|
|
144
|
+
"""
|
|
145
|
+
if rawmsg.address < 1 or rawmsg.address > 254:
|
|
146
|
+
return
|
|
147
|
+
if rawmsg.command is None:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
priority = rawmsg.priority
|
|
151
|
+
address = rawmsg.address
|
|
152
|
+
rtr = rawmsg.rtr
|
|
153
|
+
command_value = rawmsg.command
|
|
154
|
+
data = rawmsg.data_only
|
|
155
|
+
|
|
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)
|
|
187
|
+
|
|
188
|
+
# ignore broadcast
|
|
189
|
+
elif command_value in self.pdata["MessagesBroadCast"]:
|
|
190
|
+
self._log.debug(
|
|
191
|
+
"Received broadcast message {} from {}, ignoring".format(
|
|
192
|
+
self.pdata["MessageBroadCast"][str(command_value).upper()], address
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
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:
|
|
226
|
+
"""
|
|
227
|
+
load the module data
|
|
228
|
+
"""
|
|
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
|
|
@@ -20,19 +20,19 @@ class Message:
|
|
|
20
20
|
Base Velbus message
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
def __init__(self, address: int =
|
|
23
|
+
def __init__(self, address: int = 0) -> None:
|
|
24
24
|
self.priority = PRIORITY_LOW
|
|
25
|
-
self.address =
|
|
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:
|
|
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:
|
|
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]
|
|
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
|
"""
|