velbus-aio 2023.12.0__py3-none-any.whl → 2025.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scripts/parse_specs.py +156 -0
- {velbus_aio-2023.12.0.dist-info → velbus_aio-2025.11.0.dist-info}/METADATA +24 -8
- velbus_aio-2025.11.0.dist-info/RECORD +194 -0
- {velbus_aio-2023.12.0.dist-info → velbus_aio-2025.11.0.dist-info}/WHEEL +1 -1
- {velbus_aio-2023.12.0.dist-info → velbus_aio-2025.11.0.dist-info}/top_level.txt +1 -0
- velbusaio/channels.py +35 -14
- velbusaio/command_registry.py +32 -5
- velbusaio/const.py +10 -2
- velbusaio/controller.py +183 -145
- velbusaio/handler.py +286 -154
- velbusaio/helpers.py +2 -1
- velbusaio/message.py +14 -8
- velbusaio/messages/__init__.py +6 -2
- velbusaio/messages/blind_status.py +4 -2
- velbusaio/messages/bus_active.py +1 -0
- velbusaio/messages/bus_error_counter_status.py +1 -0
- velbusaio/messages/bus_error_counter_status_request.py +1 -0
- velbusaio/messages/bus_off.py +1 -0
- velbusaio/messages/channel_name_part1.py +18 -0
- velbusaio/messages/channel_name_part2.py +18 -0
- velbusaio/messages/channel_name_part3.py +18 -0
- velbusaio/messages/channel_name_request.py +2 -1
- velbusaio/messages/clear_led.py +1 -0
- velbusaio/messages/counter_status.py +1 -0
- velbusaio/messages/counter_status_request.py +1 -0
- velbusaio/messages/counter_value.py +44 -0
- velbusaio/messages/cover_down.py +2 -1
- velbusaio/messages/cover_off.py +2 -3
- velbusaio/messages/cover_position.py +3 -4
- velbusaio/messages/cover_up.py +2 -1
- velbusaio/messages/dali_device_settings.py +2 -1
- velbusaio/messages/dali_device_settings_request.py +2 -1
- velbusaio/messages/dali_dim_value_status.py +4 -1
- velbusaio/messages/dimmer_channel_status.py +5 -1
- velbusaio/messages/dimmer_status.py +13 -1
- velbusaio/messages/edge_set_color.py +1 -0
- velbusaio/messages/edge_set_custom_color.py +1 -0
- velbusaio/messages/fast_blinking_led.py +1 -0
- velbusaio/messages/forced_off.py +1 -0
- velbusaio/messages/forced_on.py +1 -0
- velbusaio/messages/interface_status_request.py +1 -0
- velbusaio/messages/ir_receiver_status.py +1 -0
- velbusaio/messages/kwh_status.py +1 -0
- velbusaio/messages/light_value_request.py +1 -0
- velbusaio/messages/memo_text.py +1 -0
- velbusaio/messages/memory_data.py +1 -0
- velbusaio/messages/memory_data_block.py +1 -0
- velbusaio/messages/memory_dump_request.py +1 -0
- velbusaio/messages/module_status.py +13 -0
- velbusaio/messages/module_status_request.py +5 -2
- velbusaio/messages/module_subtype.py +4 -3
- velbusaio/messages/module_type.py +17 -7
- velbusaio/messages/module_type_request.py +1 -0
- velbusaio/messages/psu_load.py +56 -0
- velbusaio/messages/psu_values.py +53 -0
- velbusaio/messages/push_button_status.py +1 -0
- velbusaio/messages/raw.py +1 -0
- velbusaio/messages/read_data_block_from_memory.py +1 -0
- velbusaio/messages/read_data_from_memory.py +1 -0
- velbusaio/messages/realtime_clock_status_request.py +1 -0
- velbusaio/messages/receive_buffer_full.py +1 -0
- velbusaio/messages/receive_ready.py +1 -0
- velbusaio/messages/relay_status.py +1 -0
- velbusaio/messages/restore_dimmer.py +16 -2
- velbusaio/messages/select_program.py +1 -0
- velbusaio/messages/sensor_settings_request.py +1 -0
- velbusaio/messages/sensor_temp_request.py +1 -0
- velbusaio/messages/sensor_temperature.py +1 -0
- velbusaio/messages/set_date.py +5 -10
- velbusaio/messages/set_daylight_saving.py +3 -6
- velbusaio/messages/set_dimmer.py +22 -13
- velbusaio/messages/set_led.py +1 -0
- velbusaio/messages/set_realtime_clock.py +5 -10
- velbusaio/messages/set_temperature.py +1 -0
- velbusaio/messages/slider_status.py +15 -1
- velbusaio/messages/slow_blinking_led.py +1 -0
- velbusaio/messages/start_relay_blinking_timer.py +1 -0
- velbusaio/messages/start_relay_timer.py +1 -0
- velbusaio/messages/switch_relay_off.py +1 -0
- velbusaio/messages/switch_relay_on.py +1 -0
- velbusaio/messages/switch_to_comfort.py +1 -0
- velbusaio/messages/switch_to_day.py +1 -0
- velbusaio/messages/switch_to_night.py +1 -0
- velbusaio/messages/switch_to_safe.py +1 -0
- velbusaio/messages/temp_sensor_settings_part1.py +1 -0
- velbusaio/messages/temp_sensor_settings_part2.py +1 -0
- velbusaio/messages/temp_sensor_settings_part3.py +1 -0
- velbusaio/messages/temp_sensor_settings_part4.py +1 -0
- velbusaio/messages/temp_sensor_settings_request.py +1 -0
- velbusaio/messages/temp_sensor_status.py +1 -0
- velbusaio/messages/temp_set_cooling.py +1 -0
- velbusaio/messages/temp_set_heating.py +1 -0
- velbusaio/messages/update_led_status.py +1 -0
- velbusaio/messages/very_fast_blinking_led.py +1 -0
- velbusaio/messages/write_data_to_memory.py +1 -0
- velbusaio/messages/write_memory_block.py +1 -0
- velbusaio/messages/write_module_address_and_serial_number.py +1 -0
- velbusaio/module.py +214 -102
- velbusaio/module_spec/01.json +62 -0
- velbusaio/module_spec/02.json +16 -0
- velbusaio/module_spec/03.json +23 -0
- velbusaio/module_spec/04.json +283 -0
- velbusaio/module_spec/05.json +54 -0
- velbusaio/module_spec/06.json +110 -0
- velbusaio/module_spec/07.json +16 -0
- velbusaio/module_spec/08.json +38 -0
- velbusaio/module_spec/09.json +30 -0
- velbusaio/module_spec/0A.json +58 -0
- velbusaio/module_spec/0B.json +58 -0
- velbusaio/module_spec/0C.json +18 -0
- velbusaio/module_spec/0E.json +25 -0
- velbusaio/module_spec/0F.json +16 -0
- velbusaio/module_spec/10.json +111 -0
- velbusaio/module_spec/11.json +111 -0
- velbusaio/module_spec/12.json +73 -0
- velbusaio/module_spec/13.json +4 -0
- velbusaio/module_spec/14.json +16 -0
- velbusaio/module_spec/15.json +83 -0
- velbusaio/module_spec/16.json +129 -0
- velbusaio/module_spec/17.json +129 -0
- velbusaio/module_spec/18.json +129 -0
- velbusaio/module_spec/1A.json +79 -0
- velbusaio/module_spec/1B.json +107 -0
- velbusaio/module_spec/1D.json +89 -0
- velbusaio/module_spec/1E.json +306 -0
- velbusaio/module_spec/1F.json +178 -0
- velbusaio/module_spec/20.json +178 -0
- velbusaio/module_spec/21.json +326 -0
- velbusaio/module_spec/22.json +426 -0
- velbusaio/module_spec/23.json +129 -0
- velbusaio/module_spec/24.json +30 -0
- velbusaio/module_spec/25.json +3 -0
- velbusaio/module_spec/28.json +454 -0
- velbusaio/module_spec/29.json +235 -0
- velbusaio/module_spec/2A.json +239 -0
- velbusaio/module_spec/2B.json +239 -0
- velbusaio/module_spec/2C.json +257 -0
- velbusaio/module_spec/2D.json +270 -0
- velbusaio/module_spec/2E.json +215 -0
- velbusaio/module_spec/2F.json +211 -0
- velbusaio/module_spec/30.json +58 -0
- velbusaio/module_spec/31.json +465 -0
- velbusaio/module_spec/32.json +385 -0
- velbusaio/module_spec/33.json +249 -0
- velbusaio/module_spec/34.json +313 -0
- velbusaio/module_spec/35.json +313 -0
- velbusaio/module_spec/36.json +313 -0
- velbusaio/module_spec/37.json +333 -0
- velbusaio/module_spec/38.json +111 -0
- velbusaio/module_spec/39.json +4 -0
- velbusaio/module_spec/3A.json +306 -0
- velbusaio/module_spec/3B.json +306 -0
- velbusaio/module_spec/3C.json +306 -0
- velbusaio/module_spec/3D.json +454 -0
- velbusaio/module_spec/3E.json +302 -0
- velbusaio/module_spec/3F.json +4 -0
- velbusaio/module_spec/40.json +4 -0
- velbusaio/module_spec/41.json +241 -0
- velbusaio/module_spec/42.json +4 -0
- velbusaio/module_spec/43.json +23 -0
- velbusaio/module_spec/44.json +38 -0
- velbusaio/module_spec/45.json +4 -0
- velbusaio/module_spec/48.json +111 -0
- velbusaio/module_spec/49.json +111 -0
- velbusaio/module_spec/4A.json +89 -0
- velbusaio/module_spec/4B.json +138 -0
- velbusaio/module_spec/4C.json +129 -0
- velbusaio/module_spec/4D.json +108 -0
- velbusaio/module_spec/4E.json +787 -0
- velbusaio/module_spec/4F.json +114 -0
- velbusaio/module_spec/50.json +114 -0
- velbusaio/module_spec/51.json +114 -0
- velbusaio/module_spec/52.json +456 -0
- velbusaio/module_spec/54.json +270 -0
- velbusaio/module_spec/55.json +270 -0
- velbusaio/module_spec/56.json +270 -0
- velbusaio/module_spec/57.json +260 -0
- velbusaio/module_spec/5A.json +4 -0
- velbusaio/module_spec/5B.json +4 -0
- velbusaio/module_spec/5C.json +90 -0
- velbusaio/module_spec/5F.json +78 -0
- velbusaio/module_spec/60.json +4 -0
- velbusaio/module_spec/61.json +89 -0
- velbusaio/module_spec/broadcast.json +67 -0
- velbusaio/module_spec/ignore.json +22 -0
- velbusaio/protocol.py +34 -17
- velbusaio/raw_message.py +6 -6
- velbusaio/util.py +4 -0
- velbusaio/vlp_reader.py +249 -0
- velbus_aio-2023.12.0.dist-info/RECORD +0 -103
- velbusaio/moduleprotocol/protocol.json +0 -26507
- {velbus_aio-2023.12.0.dist-info → velbus_aio-2025.11.0.dist-info/licenses}/LICENSE +0 -0
velbusaio/command_registry.py
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Command registry.
|
|
2
|
+
|
|
2
3
|
:author: Maikel Punie <maikel.punie@gmail.com> and Thomas Delaet <thomas@delaet.org>
|
|
3
4
|
"""
|
|
5
|
+
|
|
4
6
|
from __future__ import annotations
|
|
5
7
|
|
|
6
8
|
MODULE_DIRECTORY = {
|
|
7
9
|
0x01: "VMB8PB",
|
|
8
10
|
0x02: "VMB1RY",
|
|
9
11
|
0x03: "VMB1BL",
|
|
12
|
+
0x04: "VMBPSUMNGR-20",
|
|
10
13
|
0x05: "VMB6IN",
|
|
14
|
+
0x06: "VMB4LEDPWM-20",
|
|
11
15
|
0x07: "VMB1DM",
|
|
12
16
|
0x08: "VMB4RY",
|
|
13
17
|
0x09: "VMB2BL",
|
|
@@ -36,6 +40,11 @@ MODULE_DIRECTORY = {
|
|
|
36
40
|
0x20: "VMBGP4",
|
|
37
41
|
0x21: "VMBGPO",
|
|
38
42
|
0x22: "VMB7IN",
|
|
43
|
+
0x23: "VMBPIRO-10",
|
|
44
|
+
0x24: "VMB2DC-20",
|
|
45
|
+
0x25: "VMBGPTC",
|
|
46
|
+
0x26: "VMB4RYLD-20",
|
|
47
|
+
0x27: "VMB4RYNO-20",
|
|
39
48
|
0x28: "VMBGPOD",
|
|
40
49
|
0x29: "VMB1RYNOS",
|
|
41
50
|
0x2A: "VMBPIRM",
|
|
@@ -44,6 +53,7 @@ MODULE_DIRECTORY = {
|
|
|
44
53
|
0x2D: "VMBGP4PIR",
|
|
45
54
|
0x2E: "VMB1BLS",
|
|
46
55
|
0x2F: "VMBDMI-R",
|
|
56
|
+
0x30: "VMBRFR8S",
|
|
47
57
|
0x31: "VMBMETEO",
|
|
48
58
|
0x32: "VMB4AN",
|
|
49
59
|
0x33: "VMBVP01",
|
|
@@ -68,23 +78,33 @@ MODULE_DIRECTORY = {
|
|
|
68
78
|
0x48: "VMB4RYLD-10",
|
|
69
79
|
0x49: "VMB4RYNO-10",
|
|
70
80
|
0x4A: "VMB2BLE-10",
|
|
81
|
+
0x4B: "VMB8DC-20",
|
|
71
82
|
0x4C: "VMB6PB-20",
|
|
83
|
+
0x4D: "VMBPIR-20",
|
|
84
|
+
0x4E: "VMB8IN-20",
|
|
72
85
|
0x4F: "VMBEL1-20",
|
|
73
86
|
0x50: "VMBEL2-20",
|
|
74
87
|
0x51: "VMBEL4-20",
|
|
75
88
|
0x52: "VMBELO-20",
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
0x54: "VMBGP1-20",
|
|
90
|
+
0x55: "VMBGP2-20",
|
|
91
|
+
0x56: "VMBGP4-20",
|
|
92
|
+
0x57: "VMBGPO-20",
|
|
93
|
+
0x59: "VMBPIRO-20",
|
|
80
94
|
0x5A: "VMBDALI-20",
|
|
95
|
+
0x5B: "VMBSIG-20",
|
|
81
96
|
0x5C: "VMBEL4PIR-20",
|
|
82
97
|
0x5F: "VMBGP4PIR-20",
|
|
98
|
+
0x60: "VMBSIG-21",
|
|
99
|
+
0x61: "VMB2BLE-20",
|
|
83
100
|
}
|
|
84
101
|
|
|
85
102
|
|
|
86
103
|
class CommandRegistry:
|
|
104
|
+
"""Command registry class."""
|
|
105
|
+
|
|
87
106
|
def __init__(self, module_directory: dict) -> None:
|
|
107
|
+
"""Init method."""
|
|
88
108
|
self._module_directory = module_directory
|
|
89
109
|
self._default_commands = {}
|
|
90
110
|
self._overrides = {}
|
|
@@ -92,6 +112,7 @@ class CommandRegistry:
|
|
|
92
112
|
def register_command(
|
|
93
113
|
self, command_value: int, command_class: type, module_name: str | None = None
|
|
94
114
|
) -> None:
|
|
115
|
+
"""Register a command."""
|
|
95
116
|
if command_value < 0 or command_value > 255:
|
|
96
117
|
raise ValueError("Command_value should be >=0 and <=255")
|
|
97
118
|
if module_name and module_name not in self._module_directory.values():
|
|
@@ -112,6 +133,7 @@ class CommandRegistry:
|
|
|
112
133
|
def _register_override(
|
|
113
134
|
self, command_value: int, command_class: type, module_type: str
|
|
114
135
|
) -> None:
|
|
136
|
+
"""Register and override."""
|
|
115
137
|
if module_type not in self._overrides:
|
|
116
138
|
self._overrides[module_type] = {}
|
|
117
139
|
if command_value not in self._overrides[module_type]:
|
|
@@ -122,12 +144,14 @@ class CommandRegistry:
|
|
|
122
144
|
)
|
|
123
145
|
|
|
124
146
|
def _register_default(self, command_value: int, command_class: type) -> None:
|
|
147
|
+
"""Register a default command."""
|
|
125
148
|
if command_value not in self._default_commands:
|
|
126
149
|
self._default_commands[command_value] = command_class
|
|
127
150
|
else:
|
|
128
151
|
raise Exception("double registration in command registry")
|
|
129
152
|
|
|
130
153
|
def has_command(self, command_value: int, module_type: int = 0) -> bool:
|
|
154
|
+
"""Find a command."""
|
|
131
155
|
if module_type in self._overrides:
|
|
132
156
|
if command_value in self._overrides[module_type]:
|
|
133
157
|
return True
|
|
@@ -136,6 +160,7 @@ class CommandRegistry:
|
|
|
136
160
|
return False
|
|
137
161
|
|
|
138
162
|
def get_command(self, command_value: int, module_type: int = 0) -> None | type:
|
|
163
|
+
"""Search a command in the registry."""
|
|
139
164
|
if module_type in self._overrides:
|
|
140
165
|
if command_value in self._overrides[module_type]:
|
|
141
166
|
return self._overrides[module_type][command_value]
|
|
@@ -148,6 +173,8 @@ commandRegistry = CommandRegistry(MODULE_DIRECTORY)
|
|
|
148
173
|
|
|
149
174
|
|
|
150
175
|
def register(command_value: int, module_types: list[str] | None = None):
|
|
176
|
+
"""Register decorator."""
|
|
177
|
+
|
|
151
178
|
def inner_register(command_class):
|
|
152
179
|
if module_types:
|
|
153
180
|
for module_type in module_types:
|
velbusaio/const.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Constant for velbusaio.
|
|
2
|
+
|
|
2
3
|
Author: Maikel Punie <maikel.punie@gmail.com>
|
|
3
4
|
"""
|
|
5
|
+
|
|
4
6
|
from __future__ import annotations
|
|
5
7
|
|
|
6
8
|
from typing import Final
|
|
@@ -36,7 +38,13 @@ RTR: Final = 0x40
|
|
|
36
38
|
NO_RTR: Final = 0x00
|
|
37
39
|
|
|
38
40
|
CACHEDIR: Final = ".velbuscache"
|
|
39
|
-
|
|
41
|
+
|
|
42
|
+
# Module scan timeout values (in mSec)
|
|
43
|
+
SCAN_MODULETYPE_TIMEOUT: Final = 3000 # 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
|
+
)
|
|
40
48
|
|
|
41
49
|
DEVICE_CLASS_ILLUMINANCE: Final = "illuminance"
|
|
42
50
|
DEVICE_CLASS_TEMPERATURE: Final = "temperature"
|
velbusaio/controller.py
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
"""
|
|
1
|
+
"""Main interface for the velbusaio lib."""
|
|
2
|
+
|
|
4
3
|
from __future__ import annotations
|
|
5
4
|
|
|
6
5
|
import asyncio
|
|
7
6
|
import logging
|
|
8
7
|
import pathlib
|
|
9
|
-
import pickle
|
|
10
8
|
import re
|
|
11
9
|
import ssl
|
|
10
|
+
import time
|
|
12
11
|
from urllib.parse import urlparse
|
|
13
12
|
|
|
14
13
|
import serial
|
|
15
|
-
import
|
|
14
|
+
import serial_asyncio_fast
|
|
16
15
|
|
|
17
|
-
from velbusaio.channels import
|
|
18
|
-
|
|
16
|
+
from velbusaio.channels import (
|
|
17
|
+
Blind,
|
|
18
|
+
Button,
|
|
19
|
+
ButtonCounter,
|
|
20
|
+
Dimmer,
|
|
21
|
+
EdgeLit,
|
|
22
|
+
LightSensor,
|
|
23
|
+
Memo,
|
|
24
|
+
Relay,
|
|
25
|
+
SelectedProgram,
|
|
26
|
+
Sensor,
|
|
27
|
+
SensorNumber,
|
|
28
|
+
Temperature,
|
|
29
|
+
ThermostatChannel,
|
|
30
|
+
)
|
|
19
31
|
from velbusaio.exceptions import VelbusConnectionFailed
|
|
20
32
|
from velbusaio.handler import PacketHandler
|
|
21
33
|
from velbusaio.helpers import get_cache_dir
|
|
@@ -27,38 +39,42 @@ from velbusaio.messages.set_realtime_clock import SetRealtimeClock
|
|
|
27
39
|
from velbusaio.module import Module
|
|
28
40
|
from velbusaio.protocol import VelbusProtocol
|
|
29
41
|
from velbusaio.raw_message import RawMessage
|
|
42
|
+
from velbusaio.vlp_reader import VlpFile
|
|
30
43
|
|
|
31
44
|
|
|
32
45
|
class Velbus:
|
|
33
|
-
"""
|
|
34
|
-
A velbus controller
|
|
35
|
-
"""
|
|
46
|
+
"""A velbus controller."""
|
|
36
47
|
|
|
37
48
|
def __init__(
|
|
38
49
|
self,
|
|
39
50
|
dsn: str,
|
|
40
51
|
cache_dir: str = get_cache_dir(),
|
|
52
|
+
vlp_file: str | None = None,
|
|
53
|
+
one_address: int | None = None,
|
|
41
54
|
) -> None:
|
|
55
|
+
"""Init the Velbus controller."""
|
|
42
56
|
self._log = logging.getLogger("velbus")
|
|
43
57
|
|
|
44
58
|
self._protocol = VelbusProtocol(
|
|
45
59
|
message_received_callback=self._on_message_received,
|
|
46
60
|
connection_lost_callback=self._on_connection_lost,
|
|
47
|
-
end_of_scan_callback=self._on_end_of_scan,
|
|
48
61
|
)
|
|
49
62
|
self._closing = False
|
|
50
63
|
self._auto_reconnect = True
|
|
51
64
|
|
|
52
|
-
self.
|
|
53
|
-
self._handler = PacketHandler(self
|
|
65
|
+
self._destination = dsn
|
|
66
|
+
self._handler = PacketHandler(self, one_address)
|
|
54
67
|
self._modules: dict[int, Module] = {}
|
|
55
68
|
self._submodules: list[int] = []
|
|
56
|
-
self._send_queue = asyncio.Queue()
|
|
57
|
-
self.
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
self._send_queue: asyncio.Queue = asyncio.Queue()
|
|
70
|
+
self._vlp_file = vlp_file
|
|
71
|
+
self._cache_dir: str = cache_dir
|
|
72
|
+
|
|
73
|
+
def get_cache_dir(self) -> str:
|
|
74
|
+
return self._cache_dir
|
|
60
75
|
|
|
61
76
|
async def _on_message_received(self, msg: RawMessage) -> None:
|
|
77
|
+
"""On message received function."""
|
|
62
78
|
await self._handler.handle(msg)
|
|
63
79
|
|
|
64
80
|
def _on_connection_lost(self, exc: Exception) -> None:
|
|
@@ -67,108 +83,79 @@ class Velbus:
|
|
|
67
83
|
self._log.debug("Reconnecting to transport")
|
|
68
84
|
asyncio.ensure_future(self.connect())
|
|
69
85
|
|
|
70
|
-
def _on_end_of_scan(self) -> None:
|
|
71
|
-
self._handler.scan_finished()
|
|
72
|
-
|
|
73
86
|
async def add_module(
|
|
74
87
|
self,
|
|
75
88
|
addr: int,
|
|
76
89
|
typ: int,
|
|
77
|
-
data: dict,
|
|
78
90
|
serial: int | None = None,
|
|
79
91
|
memorymap: int | None = None,
|
|
80
92
|
build_year: int | None = None,
|
|
81
93
|
build_week: int | None = None,
|
|
82
94
|
) -> None:
|
|
83
|
-
"""
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
typ,
|
|
97
|
-
data,
|
|
98
|
-
serial=serial,
|
|
99
|
-
build_year=build_year,
|
|
100
|
-
build_week=build_week,
|
|
101
|
-
memorymap=memorymap,
|
|
102
|
-
cache_dir=self._cache_dir,
|
|
103
|
-
)
|
|
104
|
-
self._modules[addr].initialize(self.send)
|
|
105
|
-
await self._modules[addr].load()
|
|
95
|
+
"""Add a found module to the module cache."""
|
|
96
|
+
module = Module.factory(
|
|
97
|
+
addr,
|
|
98
|
+
typ,
|
|
99
|
+
serial=serial,
|
|
100
|
+
build_year=build_year,
|
|
101
|
+
build_week=build_week,
|
|
102
|
+
memorymap=memorymap,
|
|
103
|
+
cache_dir=self._cache_dir,
|
|
104
|
+
)
|
|
105
|
+
await module.initialize(self.send)
|
|
106
|
+
self._modules[addr] = module
|
|
107
|
+
self._log.info(f"Found module {addr}: {module}")
|
|
106
108
|
|
|
107
|
-
|
|
109
|
+
def add_submodules(self, module: Module, subList: dict[int, int]) -> None:
|
|
110
|
+
"""Add submodules address to module."""
|
|
108
111
|
for sub_num, sub_addr in subList.items():
|
|
109
112
|
if sub_addr == 0xFF:
|
|
110
113
|
continue
|
|
111
114
|
self._submodules.append(sub_addr)
|
|
112
|
-
|
|
113
|
-
self._modules[sub_addr] =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
with cfile.open("rb") as fl:
|
|
120
|
-
o = pickle.load(fl)
|
|
121
|
-
if isinstance(o, Module):
|
|
122
|
-
return o
|
|
123
|
-
except OSError:
|
|
124
|
-
pass
|
|
125
|
-
return None
|
|
115
|
+
module._sub_address[sub_num] = sub_addr
|
|
116
|
+
self._modules[sub_addr] = module
|
|
117
|
+
module.cleanupSubChannels()
|
|
118
|
+
|
|
119
|
+
def addr_is_submodule(self, addr: int) -> bool:
|
|
120
|
+
"""Check if an address is a submodule."""
|
|
121
|
+
return addr in self._submodules
|
|
126
122
|
|
|
127
123
|
def get_modules(self) -> dict:
|
|
128
|
-
"""
|
|
129
|
-
Return the module cache
|
|
130
|
-
"""
|
|
124
|
+
"""Return the module cache."""
|
|
131
125
|
return self._modules
|
|
132
126
|
|
|
133
|
-
def get_module(self, addr:
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
"""
|
|
137
|
-
if addr in self._modules.keys():
|
|
127
|
+
def get_module(self, addr: int) -> None | Module:
|
|
128
|
+
"""Get a module on an address."""
|
|
129
|
+
if addr in self._modules:
|
|
138
130
|
return self._modules[addr]
|
|
139
131
|
return None
|
|
140
132
|
|
|
141
|
-
def get_channels(self, addr:
|
|
142
|
-
"""
|
|
143
|
-
Get the channels for an address
|
|
144
|
-
"""
|
|
133
|
+
def get_channels(self, addr: int) -> None | dict:
|
|
134
|
+
"""Get the channels for an address."""
|
|
145
135
|
if addr in self._modules:
|
|
146
136
|
return (self._modules[addr]).get_channels()
|
|
147
137
|
return None
|
|
148
138
|
|
|
149
139
|
async def stop(self) -> None:
|
|
140
|
+
"""Stop the controller."""
|
|
150
141
|
self._closing = True
|
|
151
142
|
self._auto_reconnect = False
|
|
152
143
|
self._protocol.close()
|
|
153
144
|
|
|
154
|
-
async def connect(self
|
|
155
|
-
"""
|
|
156
|
-
|
|
157
|
-
"""
|
|
158
|
-
auth = None
|
|
145
|
+
async def connect(self) -> None:
|
|
146
|
+
"""Connect to the bus and load all the data."""
|
|
147
|
+
await self._handler.read_protocol_data()
|
|
159
148
|
# connect to the bus
|
|
160
|
-
if ":" in self.
|
|
149
|
+
if ":" in self._destination:
|
|
161
150
|
# tcp/ip combination
|
|
162
|
-
if not re.search(r"^[A-Za-z0-9+.\-]+://", self.
|
|
151
|
+
if not re.search(r"^[A-Za-z0-9+.\-]+://", self._destination):
|
|
163
152
|
# if no scheme, then add the tcp://
|
|
164
|
-
self.
|
|
165
|
-
parts = urlparse(self.
|
|
153
|
+
self._destination = f"tcp://{self._destination}"
|
|
154
|
+
parts = urlparse(self._destination)
|
|
166
155
|
if parts.scheme == "tls":
|
|
167
156
|
ctx = ssl._create_unverified_context()
|
|
168
157
|
else:
|
|
169
158
|
ctx = None
|
|
170
|
-
if parts.username:
|
|
171
|
-
auth = parts.username
|
|
172
159
|
try:
|
|
173
160
|
(
|
|
174
161
|
_transport,
|
|
@@ -181,74 +168,76 @@ class Velbus:
|
|
|
181
168
|
)
|
|
182
169
|
|
|
183
170
|
except (ConnectionRefusedError, OSError) as err:
|
|
184
|
-
raise VelbusConnectionFailed
|
|
171
|
+
raise VelbusConnectionFailed from err
|
|
185
172
|
else:
|
|
186
173
|
# serial port
|
|
187
174
|
try:
|
|
188
|
-
_transport, _protocol =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
175
|
+
_transport, _protocol = (
|
|
176
|
+
await serial_asyncio_fast.create_serial_connection(
|
|
177
|
+
asyncio.get_event_loop(),
|
|
178
|
+
lambda: self._protocol,
|
|
179
|
+
url=self._destination,
|
|
180
|
+
baudrate=38400,
|
|
181
|
+
bytesize=serial.EIGHTBITS,
|
|
182
|
+
parity=serial.PARITY_NONE,
|
|
183
|
+
stopbits=serial.STOPBITS_ONE,
|
|
184
|
+
xonxoff=0,
|
|
185
|
+
rtscts=1,
|
|
186
|
+
)
|
|
198
187
|
)
|
|
199
188
|
except (FileNotFoundError, serial.SerialException) as err:
|
|
200
|
-
raise VelbusConnectionFailed
|
|
201
|
-
|
|
202
|
-
|
|
189
|
+
raise VelbusConnectionFailed from err
|
|
190
|
+
|
|
191
|
+
async def start(self) -> None:
|
|
203
192
|
# if auth is required send the auth key
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
parts = urlparse(self._destination)
|
|
194
|
+
if parts.username:
|
|
195
|
+
await self._protocol.write_auth_key(parts.username)
|
|
206
196
|
|
|
207
|
-
|
|
208
|
-
|
|
197
|
+
if self._vlp_file:
|
|
198
|
+
# use the vlp file to load the modules
|
|
199
|
+
vlp = VlpFile(self._vlp_file)
|
|
200
|
+
await vlp.read()
|
|
201
|
+
for mod_data in vlp.get():
|
|
202
|
+
# Convert hex address string to decimal integer
|
|
203
|
+
addr = mod_data.get_addr().split(",")
|
|
204
|
+
decimal_addr = int(addr[0], 16)
|
|
205
|
+
await self.add_module(
|
|
206
|
+
decimal_addr,
|
|
207
|
+
mod_data.get_type(),
|
|
208
|
+
serial=mod_data.get_serial(),
|
|
209
|
+
memorymap=mod_data.get_memory(),
|
|
210
|
+
build_year=int(mod_data.get_build()[0:2]),
|
|
211
|
+
build_week=int(mod_data.get_build()[2:4]),
|
|
212
|
+
)
|
|
213
|
+
# handle submodules
|
|
214
|
+
if len(addr) > 1:
|
|
215
|
+
self.add_submodules(
|
|
216
|
+
self._modules[decimal_addr], dict(enumerate(addr[1:]))
|
|
217
|
+
)
|
|
218
|
+
# load module data, special for dali
|
|
219
|
+
if mod_data.get_type() == 0x45 or mod_data.get_type() == 0x5A:
|
|
220
|
+
await self._modules[decimal_addr].load()
|
|
221
|
+
else:
|
|
222
|
+
module = self._modules[decimal_addr]
|
|
223
|
+
await module.load_from_vlp(mod_data)
|
|
224
|
+
await module.wait_for_status_messages()
|
|
225
|
+
else:
|
|
226
|
+
# make sure the cachedir exists
|
|
227
|
+
pathlib.Path(self._cache_dir).mkdir(parents=True, exist_ok=True)
|
|
228
|
+
# scan the bus
|
|
229
|
+
await self._handler.scan()
|
|
209
230
|
|
|
210
231
|
async def scan(self) -> None:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
msg = ModuleTypeRequestMessage(addr)
|
|
214
|
-
await self.send(msg)
|
|
215
|
-
await self._handler._scan_complete_event.wait()
|
|
216
|
-
# calculate how long to wait
|
|
217
|
-
calc_timeout = len(self._modules) * 30
|
|
218
|
-
if calc_timeout < LOAD_TIMEOUT:
|
|
219
|
-
timeout = calc_timeout
|
|
220
|
-
else:
|
|
221
|
-
timeout = LOAD_TIMEOUT
|
|
222
|
-
# create a task to wait until we have all modules loaded
|
|
223
|
-
tsk = asyncio.Task(self._check_if_modules_are_loaded())
|
|
224
|
-
try:
|
|
225
|
-
await asyncio.wait_for(tsk, timeout=timeout)
|
|
226
|
-
except asyncio.TimeoutError:
|
|
227
|
-
self._log.error(
|
|
228
|
-
f"Not all modules are loaded within a timeout of {LOAD_TIMEOUT} seconds, continuing with the loaded modules"
|
|
229
|
-
)
|
|
232
|
+
"""Service endpoint to restart the scan"""
|
|
233
|
+
await self._handler.scan(True)
|
|
230
234
|
|
|
231
|
-
async def
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"""
|
|
235
|
-
while True:
|
|
236
|
-
mods_loaded = 0
|
|
237
|
-
for mod in (self.get_modules()).values():
|
|
238
|
-
if mod.is_loaded():
|
|
239
|
-
mods_loaded += 1
|
|
240
|
-
else:
|
|
241
|
-
self._log.warning(f"Waiting for module {mod._address}")
|
|
242
|
-
if mods_loaded == len(self.get_modules()):
|
|
243
|
-
self._log.info("All modules loaded")
|
|
244
|
-
return
|
|
245
|
-
self._log.info("Not all modules loaded yet, waiting 15 seconds")
|
|
246
|
-
await asyncio.sleep(15)
|
|
235
|
+
async def sendTypeRequestMessage(self, address: int) -> None:
|
|
236
|
+
msg = ModuleTypeRequestMessage(address)
|
|
237
|
+
await self.send(msg)
|
|
247
238
|
|
|
248
239
|
async def send(self, msg: Message) -> None:
|
|
249
|
-
"""
|
|
250
|
-
Send a packet
|
|
251
|
-
"""
|
|
240
|
+
"""Send a packet."""
|
|
252
241
|
await self._protocol.send_message(
|
|
253
242
|
RawMessage(
|
|
254
243
|
priority=msg.priority,
|
|
@@ -258,7 +247,53 @@ class Velbus:
|
|
|
258
247
|
)
|
|
259
248
|
)
|
|
260
249
|
|
|
261
|
-
def
|
|
250
|
+
def get_all_sensor(
|
|
251
|
+
self,
|
|
252
|
+
) -> list[ButtonCounter | Temperature | LightSensor | SensorNumber]:
|
|
253
|
+
return self._get_all("sensor")
|
|
254
|
+
|
|
255
|
+
def get_all_switch(self) -> list[Relay]:
|
|
256
|
+
return self._get_all("switch")
|
|
257
|
+
|
|
258
|
+
def get_all_binary_sensor(self) -> list[Button]:
|
|
259
|
+
return self._get_all("binary_sensor")
|
|
260
|
+
|
|
261
|
+
def get_all_button(self) -> list[Button | ButtonCounter]:
|
|
262
|
+
return self._get_all("button")
|
|
263
|
+
|
|
264
|
+
def get_all_climate(self) -> list[Temperature]:
|
|
265
|
+
return self._get_all("climate")
|
|
266
|
+
|
|
267
|
+
def get_all_cover(self) -> list[Blind]:
|
|
268
|
+
return self._get_all("cover")
|
|
269
|
+
|
|
270
|
+
def get_all_select(self) -> list[SelectedProgram]:
|
|
271
|
+
return self._get_all("select")
|
|
272
|
+
|
|
273
|
+
def get_all_light(self) -> list[Dimmer]:
|
|
274
|
+
return self._get_all("light")
|
|
275
|
+
|
|
276
|
+
def get_all_led(self) -> list[Button]:
|
|
277
|
+
return self._get_all("led")
|
|
278
|
+
|
|
279
|
+
def _get_all(
|
|
280
|
+
self, class_name: str
|
|
281
|
+
) -> list[
|
|
282
|
+
Blind
|
|
283
|
+
| Button
|
|
284
|
+
| ButtonCounter
|
|
285
|
+
| Sensor
|
|
286
|
+
| ThermostatChannel
|
|
287
|
+
| Dimmer
|
|
288
|
+
| Temperature
|
|
289
|
+
| SensorNumber
|
|
290
|
+
| LightSensor
|
|
291
|
+
| Relay
|
|
292
|
+
| EdgeLit
|
|
293
|
+
| Memo
|
|
294
|
+
| SelectedProgram
|
|
295
|
+
]:
|
|
296
|
+
"""Get all channels."""
|
|
262
297
|
lst = []
|
|
263
298
|
for addr, mod in (self.get_modules()).items():
|
|
264
299
|
if addr in self._submodules:
|
|
@@ -269,9 +304,12 @@ class Velbus:
|
|
|
269
304
|
return lst
|
|
270
305
|
|
|
271
306
|
async def sync_clock(self) -> None:
|
|
272
|
-
"""
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
await self.send(
|
|
276
|
-
await self.send(
|
|
277
|
-
|
|
307
|
+
"""Will send all the needed messages to sync the clock."""
|
|
308
|
+
lclt = time.localtime()
|
|
309
|
+
await self.send(SetRealtimeClock(wday=lclt[6], hour=lclt[3], min=lclt[4]))
|
|
310
|
+
await self.send(SetDate(day=lclt[2], mon=lclt[1], year=lclt[0]))
|
|
311
|
+
await self.send(SetDaylightSaving(ds=not lclt[8]))
|
|
312
|
+
|
|
313
|
+
async def wait_on_all_messages_sent_async(self) -> None:
|
|
314
|
+
"""Wait for all messages to be sent."""
|
|
315
|
+
await self._protocol.wait_on_all_messages_sent_async()
|