velbus-aio 2021.8.7__py3-none-any.whl → 2025.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scripts/parse_specs.py +156 -0
- velbus_aio-2025.11.0.dist-info/METADATA +71 -0
- velbus_aio-2025.11.0.dist-info/RECORD +194 -0
- {velbus_aio-2021.8.7.dist-info → velbus_aio-2025.11.0.dist-info}/WHEEL +1 -1
- velbus_aio-2025.11.0.dist-info/top_level.txt +3 -0
- velbusaio/channels.py +443 -109
- velbusaio/command_registry.py +126 -13
- velbusaio/const.py +36 -12
- velbusaio/controller.py +252 -177
- velbusaio/discovery.py +2 -2
- velbusaio/exceptions.py +22 -0
- velbusaio/handler.py +311 -145
- velbusaio/helpers.py +6 -18
- velbusaio/message.py +46 -132
- velbusaio/messages/__init__.py +12 -2
- velbusaio/messages/blind_status.py +16 -25
- velbusaio/messages/bus_active.py +3 -9
- velbusaio/messages/bus_error_counter_status.py +3 -4
- velbusaio/messages/bus_error_counter_status_request.py +3 -4
- velbusaio/messages/bus_off.py +3 -4
- velbusaio/messages/channel_name_part1.py +49 -33
- velbusaio/messages/channel_name_part2.py +49 -33
- velbusaio/messages/channel_name_part3.py +49 -33
- velbusaio/messages/channel_name_request.py +26 -12
- velbusaio/messages/clear_led.py +3 -4
- velbusaio/messages/counter_status.py +3 -17
- velbusaio/messages/counter_status_request.py +6 -6
- velbusaio/messages/counter_value.py +44 -0
- velbusaio/messages/cover_down.py +4 -29
- velbusaio/messages/cover_off.py +5 -29
- velbusaio/messages/cover_position.py +4 -19
- velbusaio/messages/cover_up.py +4 -27
- velbusaio/messages/dali_device_settings.py +178 -0
- velbusaio/messages/dali_device_settings_request.py +53 -0
- velbusaio/messages/dali_dim_value_status.py +44 -0
- velbusaio/messages/dimmer_channel_status.py +6 -19
- velbusaio/messages/dimmer_status.py +14 -31
- velbusaio/messages/edge_set_color.py +114 -0
- velbusaio/messages/edge_set_custom_color.py +56 -0
- velbusaio/messages/fast_blinking_led.py +3 -4
- velbusaio/messages/forced_off.py +3 -4
- velbusaio/messages/forced_on.py +3 -4
- velbusaio/messages/interface_status_request.py +3 -4
- velbusaio/messages/ir_receiver_status.py +18 -0
- velbusaio/messages/kwh_status.py +3 -19
- velbusaio/messages/light_value_request.py +3 -4
- velbusaio/messages/memo_text.py +3 -5
- velbusaio/messages/memory_data.py +3 -16
- velbusaio/messages/memory_data_block.py +3 -4
- velbusaio/messages/memory_dump_request.py +3 -4
- velbusaio/messages/module_status.py +107 -55
- velbusaio/messages/module_status_request.py +7 -6
- velbusaio/messages/module_subtype.py +11 -19
- velbusaio/messages/module_type.py +132 -21
- 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 +3 -16
- velbusaio/messages/raw.py +74 -0
- velbusaio/messages/read_data_block_from_memory.py +3 -4
- velbusaio/messages/read_data_from_memory.py +3 -4
- velbusaio/messages/realtime_clock_status_request.py +3 -4
- velbusaio/messages/receive_buffer_full.py +3 -4
- velbusaio/messages/receive_ready.py +3 -4
- velbusaio/messages/relay_status.py +13 -42
- velbusaio/messages/restore_dimmer.py +33 -24
- velbusaio/messages/select_program.py +35 -0
- velbusaio/messages/sensor_settings_request.py +3 -4
- velbusaio/messages/sensor_temp_request.py +3 -4
- velbusaio/messages/sensor_temperature.py +15 -19
- velbusaio/messages/set_date.py +10 -30
- velbusaio/messages/set_daylight_saving.py +8 -24
- velbusaio/messages/set_dimmer.py +43 -41
- velbusaio/messages/set_led.py +3 -4
- velbusaio/messages/set_realtime_clock.py +10 -30
- velbusaio/messages/set_temperature.py +3 -4
- velbusaio/messages/slider_status.py +16 -20
- velbusaio/messages/slow_blinking_led.py +3 -4
- velbusaio/messages/start_relay_blinking_timer.py +3 -4
- velbusaio/messages/start_relay_timer.py +3 -4
- velbusaio/messages/switch_relay_off.py +3 -16
- velbusaio/messages/switch_relay_on.py +3 -16
- velbusaio/messages/switch_to_comfort.py +4 -15
- velbusaio/messages/switch_to_day.py +4 -15
- velbusaio/messages/switch_to_night.py +4 -15
- velbusaio/messages/switch_to_safe.py +4 -15
- velbusaio/messages/temp_sensor_settings_part1.py +3 -4
- velbusaio/messages/temp_sensor_settings_part2.py +27 -0
- velbusaio/messages/temp_sensor_settings_part3.py +27 -0
- velbusaio/messages/temp_sensor_settings_part4.py +27 -0
- velbusaio/messages/temp_sensor_settings_request.py +3 -4
- velbusaio/messages/temp_sensor_status.py +34 -35
- velbusaio/messages/temp_set_cooling.py +3 -13
- velbusaio/messages/temp_set_heating.py +3 -13
- velbusaio/messages/update_led_status.py +3 -4
- velbusaio/messages/very_fast_blinking_led.py +3 -4
- velbusaio/messages/write_data_to_memory.py +3 -4
- velbusaio/messages/write_memory_block.py +3 -4
- velbusaio/messages/write_module_address_and_serial_number.py +3 -4
- velbusaio/module.py +680 -158
- 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 +243 -0
- velbusaio/py.typed +0 -0
- velbusaio/raw_message.py +149 -0
- velbusaio/util.py +55 -0
- velbusaio/vlp_reader.py +249 -0
- velbus_aio-2021.8.7.dist-info/METADATA +0 -66
- velbus_aio-2021.8.7.dist-info/RECORD +0 -90
- velbus_aio-2021.8.7.dist-info/top_level.txt +0 -1
- velbusaio/messages/meteo_raw.py +0 -52
- velbusaio/module_registry.py +0 -64
- velbusaio/moduleprotocol/protocol.json +0 -25540
- velbusaio/parser.py +0 -142
- {velbus_aio-2021.8.7.dist-info → velbus_aio-2025.11.0.dist-info/licenses}/LICENSE +0 -0
velbusaio/channels.py
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
"""
|
|
2
2
|
author: Maikel Punie <maikel.punie@gmail.com>
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
|
|
6
|
-
import
|
|
7
|
+
import asyncio
|
|
8
|
+
import math
|
|
7
9
|
import string
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
|
8
11
|
|
|
9
12
|
from velbusaio.command_registry import commandRegistry
|
|
10
13
|
from velbusaio.const import (
|
|
11
14
|
DEVICE_CLASS_ILLUMINANCE,
|
|
12
15
|
DEVICE_CLASS_TEMPERATURE,
|
|
13
16
|
ENERGY_KILO_WATT_HOUR,
|
|
14
|
-
ENERGY_WATT_HOUR,
|
|
15
17
|
TEMP_CELSIUS,
|
|
16
|
-
VOLUME_CUBIC_METER,
|
|
17
18
|
VOLUME_CUBIC_METER_HOUR,
|
|
18
|
-
VOLUME_LITERS,
|
|
19
19
|
VOLUME_LITERS_HOUR,
|
|
20
20
|
)
|
|
21
|
-
from velbusaio.
|
|
22
|
-
from velbusaio.messages.
|
|
21
|
+
from velbusaio.message import Message
|
|
22
|
+
from velbusaio.messages.edge_set_color import CustomColorPriority, SetEdgeColorMessage
|
|
23
|
+
from velbusaio.messages.module_status import PROGRAM_SELECTION
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from velbusaio.module import Module
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
class Channel:
|
|
@@ -28,10 +32,20 @@ class Channel:
|
|
|
28
32
|
This is the basic abstract class of a velbus channel
|
|
29
33
|
"""
|
|
30
34
|
|
|
31
|
-
def __init__(
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
module: Module,
|
|
38
|
+
num: int,
|
|
39
|
+
name: str,
|
|
40
|
+
nameEditable: bool,
|
|
41
|
+
subDevice: bool,
|
|
42
|
+
writer: Callable[[Message], Awaitable[None]],
|
|
43
|
+
address: int,
|
|
44
|
+
):
|
|
32
45
|
self._num = num
|
|
33
46
|
self._module = module
|
|
34
47
|
self._name = name
|
|
48
|
+
self._subDevice = subDevice
|
|
35
49
|
if not nameEditable:
|
|
36
50
|
self._is_loaded = True
|
|
37
51
|
else:
|
|
@@ -41,7 +55,7 @@ class Channel:
|
|
|
41
55
|
self._on_status_update = []
|
|
42
56
|
self._name_parts = {}
|
|
43
57
|
|
|
44
|
-
def get_module_type(self) ->
|
|
58
|
+
def get_module_type(self) -> int:
|
|
45
59
|
return self._module.get_type()
|
|
46
60
|
|
|
47
61
|
def get_module_type_name(self) -> str:
|
|
@@ -50,8 +64,16 @@ class Channel:
|
|
|
50
64
|
def get_module_serial(self) -> str:
|
|
51
65
|
return self._module.get_serial()
|
|
52
66
|
|
|
53
|
-
def get_module_address(self) -> int:
|
|
54
|
-
|
|
67
|
+
def get_module_address(self, chan_type: str = "") -> int:
|
|
68
|
+
"""Return (sub)module address for channel"""
|
|
69
|
+
if chan_type == "Button" and self._num > 24:
|
|
70
|
+
return self._module.get_addresses()[3]
|
|
71
|
+
elif chan_type == "Button" and self._num > 16:
|
|
72
|
+
return self._module.get_addresses()[2]
|
|
73
|
+
elif chan_type == "Button" and self._num > 8:
|
|
74
|
+
return self._module.get_addresses()[1]
|
|
75
|
+
else:
|
|
76
|
+
return self._address
|
|
55
77
|
|
|
56
78
|
def get_module_sw_version(self) -> str:
|
|
57
79
|
return self._module.get_sw_version()
|
|
@@ -60,33 +82,48 @@ class Channel:
|
|
|
60
82
|
return self._num
|
|
61
83
|
|
|
62
84
|
def get_full_name(self) -> str:
|
|
85
|
+
if self._subDevice:
|
|
86
|
+
return f"{self._module.get_name()} ({self._module.get_type_name()}) - {self._name}"
|
|
63
87
|
return f"{self._module.get_name()} ({self._module.get_type_name()})"
|
|
64
88
|
|
|
65
89
|
def is_loaded(self) -> bool:
|
|
66
90
|
"""
|
|
67
91
|
Is this channel loaded
|
|
68
|
-
|
|
69
|
-
:return: Boolean
|
|
70
92
|
"""
|
|
71
93
|
return self._is_loaded
|
|
72
94
|
|
|
73
95
|
def is_counter_channel(self) -> bool:
|
|
74
96
|
return False
|
|
75
97
|
|
|
98
|
+
def is_temperature(self) -> bool:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def is_sub_device(self) -> bool:
|
|
102
|
+
return self._subDevice
|
|
103
|
+
|
|
76
104
|
def get_name(self) -> str:
|
|
77
105
|
"""
|
|
78
106
|
:return: the channel name
|
|
79
107
|
"""
|
|
80
108
|
return self._name
|
|
81
109
|
|
|
82
|
-
def
|
|
110
|
+
def set_name_char(self, pos: int, char: int) -> None:
|
|
111
|
+
self._is_loaded = True
|
|
112
|
+
self._name_parts = {}
|
|
113
|
+
# make sure the string is long enough
|
|
114
|
+
while len(self._name) < int(pos):
|
|
115
|
+
self._name += " "
|
|
116
|
+
# store the char on correct pos
|
|
117
|
+
self._name = self._name[: int(pos)] + chr(char) + self._name[int(pos) + 1 :]
|
|
118
|
+
|
|
119
|
+
def set_name_part(self, part: int, name: str) -> None:
|
|
83
120
|
"""
|
|
84
121
|
Set a part of the channel name
|
|
85
122
|
"""
|
|
86
123
|
# if int(part) not in self._name_parts:
|
|
87
124
|
# return
|
|
88
125
|
self._name_parts[int(part)] = name
|
|
89
|
-
if
|
|
126
|
+
if len(self._name_parts) == 3:
|
|
90
127
|
self._generate_name()
|
|
91
128
|
|
|
92
129
|
def _generate_name(self) -> None:
|
|
@@ -96,76 +133,137 @@ class Channel:
|
|
|
96
133
|
name = self._name_parts[1] + self._name_parts[2] + self._name_parts[3]
|
|
97
134
|
self._name = "".join(filter(lambda x: x in string.printable, name))
|
|
98
135
|
self._is_loaded = True
|
|
99
|
-
self._name_parts =
|
|
136
|
+
self._name_parts = {}
|
|
100
137
|
|
|
101
138
|
def __getstate__(self):
|
|
102
139
|
d = self.__dict__
|
|
103
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
k: d[k]
|
|
142
|
+
for k in d
|
|
143
|
+
if k != "_writer" and k != "_on_status_update" and k != "_name_parts"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def to_cache(self) -> dict:
|
|
147
|
+
dst = {
|
|
148
|
+
"name": self._name,
|
|
149
|
+
"type": type(self).__name__,
|
|
150
|
+
"subdevice": self._subDevice,
|
|
151
|
+
}
|
|
152
|
+
if hasattr(self, "_Unit"):
|
|
153
|
+
dst["Unit"] = self._Unit
|
|
154
|
+
return dst
|
|
104
155
|
|
|
105
156
|
def __setstate__(self, state):
|
|
106
157
|
self.__dict__.update(state)
|
|
107
158
|
self._on_status_update = []
|
|
159
|
+
self._name_parts = {}
|
|
108
160
|
|
|
109
|
-
def __repr__(self):
|
|
161
|
+
def __repr__(self) -> str:
|
|
110
162
|
items = []
|
|
111
163
|
for k, v in self.__dict__.items():
|
|
112
|
-
if k not in ["_module", "_writer", "_name_parts"]:
|
|
164
|
+
if k not in ["_module", "_writer", "_name_parts", "_class"]:
|
|
113
165
|
items.append(f"{k} = {v!r}")
|
|
114
166
|
return "{}[{}]".format(type(self), ", ".join(items))
|
|
115
167
|
|
|
116
|
-
def __str__(self):
|
|
168
|
+
def __str__(self) -> str:
|
|
117
169
|
return self.__repr__()
|
|
118
170
|
|
|
171
|
+
def get_channel_info(self) -> dict[str, Any]:
|
|
172
|
+
data = {}
|
|
173
|
+
for key, value in self.__dict__.items():
|
|
174
|
+
data["type"] = self.__class__.__name__
|
|
175
|
+
if key not in ["_module", "_writer", "_name_parts", "_on_status_update"]:
|
|
176
|
+
data[key.replace("_", "", 1)] = value
|
|
177
|
+
return data
|
|
178
|
+
|
|
119
179
|
async def update(self, data: dict) -> None:
|
|
120
180
|
"""
|
|
121
181
|
Set the attributes of this channel
|
|
122
182
|
"""
|
|
123
|
-
for key,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
183
|
+
for key, new_val in data.items():
|
|
184
|
+
cur_val = getattr(self, f"_{key}", None)
|
|
185
|
+
if cur_val is None or cur_val != new_val:
|
|
186
|
+
setattr(self, f"_{key}", new_val)
|
|
187
|
+
for m in self._on_status_update:
|
|
188
|
+
await m()
|
|
127
189
|
|
|
128
|
-
def get_categories(self) -> list:
|
|
190
|
+
def get_categories(self) -> list[str]:
|
|
129
191
|
"""
|
|
130
|
-
Get the categories (for
|
|
192
|
+
Get the categories (mainly for home-assistant)
|
|
131
193
|
"""
|
|
132
194
|
# COMPONENT_TYPES = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"]
|
|
133
195
|
return []
|
|
134
196
|
|
|
135
|
-
def on_status_update(self, meth:
|
|
197
|
+
def on_status_update(self, meth: Callable[[], Awaitable[None]]) -> None:
|
|
136
198
|
self._on_status_update.append(meth)
|
|
137
199
|
|
|
200
|
+
def remove_on_status_update(self, meth: Callable[[], Awaitable[None]]) -> None:
|
|
201
|
+
self._on_status_update.remove(meth)
|
|
202
|
+
|
|
203
|
+
def get_counter_state(self) -> int:
|
|
204
|
+
raise NotImplementedError()
|
|
205
|
+
|
|
206
|
+
def get_counter_unit(self) -> str:
|
|
207
|
+
raise NotImplementedError()
|
|
208
|
+
|
|
209
|
+
def get_max(self) -> int:
|
|
210
|
+
raise NotImplementedError()
|
|
211
|
+
|
|
212
|
+
def get_min(self) -> int:
|
|
213
|
+
raise NotImplementedError()
|
|
214
|
+
|
|
215
|
+
def is_water(self) -> bool:
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
async def press(self) -> None:
|
|
219
|
+
raise NotImplementedError()
|
|
220
|
+
|
|
138
221
|
|
|
139
222
|
class Blind(Channel):
|
|
140
223
|
"""
|
|
141
224
|
A blind channel
|
|
142
|
-
HASS OK
|
|
143
225
|
"""
|
|
144
226
|
|
|
145
227
|
_state = None
|
|
228
|
+
# State reports the direction of *movement*: moving up, moving down or stopped
|
|
146
229
|
_position = None
|
|
230
|
+
# Position reporting is not supported by VMBxBL modules (only in BLE/BLS)
|
|
147
231
|
|
|
148
|
-
def get_categories(self) -> list:
|
|
232
|
+
def get_categories(self) -> list[str]:
|
|
149
233
|
return ["cover"]
|
|
150
234
|
|
|
151
|
-
def get_position(self) ->
|
|
235
|
+
def get_position(self) -> int | None:
|
|
152
236
|
return self._position
|
|
153
237
|
|
|
154
238
|
def get_state(self) -> str:
|
|
155
239
|
return self._state
|
|
156
240
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
return True
|
|
160
|
-
return False
|
|
241
|
+
def is_opening(self) -> bool:
|
|
242
|
+
return self._state == 0x01
|
|
161
243
|
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
244
|
+
def is_closing(self) -> bool:
|
|
245
|
+
return self._state == 0x02
|
|
246
|
+
|
|
247
|
+
def is_stopped(self) -> bool:
|
|
248
|
+
return self._state == 0x00
|
|
249
|
+
|
|
250
|
+
def is_closed(self) -> bool | None:
|
|
251
|
+
"""Report if the blind is fully closed."""
|
|
252
|
+
if self._position is None:
|
|
253
|
+
return None
|
|
254
|
+
# else:
|
|
255
|
+
return self._position == 100
|
|
256
|
+
|
|
257
|
+
def is_open(self) -> bool | None:
|
|
258
|
+
"""Report if the blind is fully open."""
|
|
259
|
+
if self._position is None:
|
|
260
|
+
return None
|
|
261
|
+
return self._position == 0
|
|
166
262
|
|
|
167
263
|
def support_position(self) -> bool:
|
|
168
|
-
|
|
264
|
+
# position will be populated after the first BlindStatusNgMessage (during module load)
|
|
265
|
+
# For VMBxBL modules, position will remain None and not be overwritten
|
|
266
|
+
return self._position is not None
|
|
169
267
|
|
|
170
268
|
async def open(self) -> None:
|
|
171
269
|
cls = commandRegistry.get_command(0x05, self._module.get_type())
|
|
@@ -186,6 +284,11 @@ class Blind(Channel):
|
|
|
186
284
|
await self._writer(msg)
|
|
187
285
|
|
|
188
286
|
async def set_position(self, position: int) -> None:
|
|
287
|
+
# may not be supported by the module
|
|
288
|
+
if position == 100:
|
|
289
|
+
# at least VMB1BLS ignores command 0x1C with position 0x64
|
|
290
|
+
await self.close()
|
|
291
|
+
return
|
|
189
292
|
cls = commandRegistry.get_command(0x1C, self._module.get_type())
|
|
190
293
|
msg = cls(self._address)
|
|
191
294
|
msg.channel = self._num
|
|
@@ -196,15 +299,17 @@ class Blind(Channel):
|
|
|
196
299
|
class Button(Channel):
|
|
197
300
|
"""
|
|
198
301
|
A Button channel
|
|
199
|
-
HASS OK
|
|
200
302
|
"""
|
|
201
303
|
|
|
202
304
|
_enabled = True
|
|
203
305
|
_closed = False
|
|
204
306
|
_led_state = None
|
|
307
|
+
_long = False
|
|
205
308
|
|
|
206
|
-
def get_categories(self) -> list:
|
|
207
|
-
|
|
309
|
+
def get_categories(self) -> list[str]:
|
|
310
|
+
if self._enabled:
|
|
311
|
+
return ["binary_sensor", "led", "button"]
|
|
312
|
+
return []
|
|
208
313
|
|
|
209
314
|
def is_closed(self) -> bool:
|
|
210
315
|
"""
|
|
@@ -223,8 +328,6 @@ class Button(Channel):
|
|
|
223
328
|
async def set_led_state(self, state: str) -> None:
|
|
224
329
|
"""
|
|
225
330
|
Set led
|
|
226
|
-
|
|
227
|
-
:return: None
|
|
228
331
|
"""
|
|
229
332
|
if state == "on":
|
|
230
333
|
code = 0xF6
|
|
@@ -236,29 +339,53 @@ class Button(Channel):
|
|
|
236
339
|
code = 0xF5
|
|
237
340
|
else:
|
|
238
341
|
return
|
|
342
|
+
|
|
343
|
+
_mod_add = self.get_module_address("Button")
|
|
344
|
+
_chn_num = self._num - self._module.calc_channel_offset(_mod_add)
|
|
239
345
|
cls = commandRegistry.get_command(code, self._module.get_type())
|
|
240
|
-
msg = cls(
|
|
241
|
-
msg.leds = [
|
|
346
|
+
msg = cls(_mod_add)
|
|
347
|
+
msg.leds = [_chn_num]
|
|
242
348
|
await self._writer(msg)
|
|
243
349
|
await self.update({"led_state": state})
|
|
244
350
|
|
|
351
|
+
async def press(self) -> None:
|
|
352
|
+
"""
|
|
353
|
+
Press the button
|
|
354
|
+
"""
|
|
355
|
+
_mod_add = self.get_module_address("Button")
|
|
356
|
+
_chn_num = self._num - self._module.calc_channel_offset(_mod_add)
|
|
357
|
+
# send the just pressed
|
|
358
|
+
cls = commandRegistry.get_command(0x00, self._module.get_type())
|
|
359
|
+
msg = cls(_mod_add)
|
|
360
|
+
msg.closed = [_chn_num]
|
|
361
|
+
await self._writer(msg)
|
|
362
|
+
# wait
|
|
363
|
+
await asyncio.sleep(0.3)
|
|
364
|
+
# send the just released
|
|
365
|
+
msg = cls(_mod_add)
|
|
366
|
+
msg.opened = [_chn_num]
|
|
367
|
+
await self._writer(msg)
|
|
368
|
+
|
|
245
369
|
|
|
246
370
|
class ButtonCounter(Button):
|
|
247
371
|
"""
|
|
248
372
|
A ButtonCounter channel
|
|
249
373
|
This channel can act as a button and as a counter
|
|
250
|
-
|
|
374
|
+
=> standard this is the calculated value
|
|
375
|
+
=> is_counter this is the numeric value
|
|
251
376
|
"""
|
|
252
377
|
|
|
253
378
|
_Unit = None
|
|
254
379
|
_pulses = None
|
|
255
380
|
_counter = None
|
|
256
381
|
_delay = None
|
|
382
|
+
_power = None
|
|
383
|
+
_energy = None
|
|
257
384
|
|
|
258
|
-
def get_categories(self) -> list:
|
|
385
|
+
def get_categories(self) -> list[str]:
|
|
259
386
|
if self._counter:
|
|
260
387
|
return ["sensor"]
|
|
261
|
-
return ["binary_sensor"]
|
|
388
|
+
return ["binary_sensor", "button"]
|
|
262
389
|
|
|
263
390
|
def is_counter_channel(self) -> bool:
|
|
264
391
|
if self._counter:
|
|
@@ -266,11 +393,13 @@ class ButtonCounter(Button):
|
|
|
266
393
|
return False
|
|
267
394
|
|
|
268
395
|
def get_state(self) -> int:
|
|
269
|
-
|
|
396
|
+
if self._energy:
|
|
397
|
+
return self._energy
|
|
270
398
|
# if we don't know the delay
|
|
271
399
|
# or we don't know the unit
|
|
272
|
-
# or the
|
|
400
|
+
# or the delay is the max value
|
|
273
401
|
# we always return 0
|
|
402
|
+
val = 0
|
|
274
403
|
if not self._delay or not self._Unit or self._delay == 0xFFFF:
|
|
275
404
|
return round(0, 2)
|
|
276
405
|
if self._Unit == VOLUME_LITERS_HOUR:
|
|
@@ -283,42 +412,74 @@ class ButtonCounter(Button):
|
|
|
283
412
|
val = 0
|
|
284
413
|
return round(val, 2)
|
|
285
414
|
|
|
286
|
-
def get_unit(self) -> str:
|
|
287
|
-
|
|
415
|
+
def get_unit(self) -> str | None:
|
|
416
|
+
if self._Unit == VOLUME_LITERS_HOUR:
|
|
417
|
+
return "L"
|
|
418
|
+
if self._Unit == VOLUME_CUBIC_METER_HOUR:
|
|
419
|
+
return "m3"
|
|
420
|
+
if self._Unit == ENERGY_KILO_WATT_HOUR:
|
|
421
|
+
return "W"
|
|
422
|
+
return None
|
|
288
423
|
|
|
289
424
|
def get_counter_state(self) -> int:
|
|
425
|
+
if self._power:
|
|
426
|
+
return self._power
|
|
290
427
|
return round((self._counter / self._pulses), 2)
|
|
291
428
|
|
|
292
429
|
def get_counter_unit(self) -> str:
|
|
293
|
-
return
|
|
430
|
+
return self._Unit
|
|
431
|
+
|
|
432
|
+
def is_water(self) -> bool:
|
|
433
|
+
if self._counter and self._Unit == VOLUME_LITERS_HOUR:
|
|
434
|
+
return True
|
|
435
|
+
return False
|
|
294
436
|
|
|
295
437
|
|
|
296
438
|
class Sensor(Button):
|
|
297
439
|
"""
|
|
298
440
|
A Sensor channel
|
|
299
|
-
|
|
300
|
-
This is a bit wierd, but this happens because of code sharing with openhab
|
|
441
|
+
This is a bit weird, but this happens because of code sharing with openhab
|
|
301
442
|
A sensor in this case is actually a Button
|
|
302
443
|
"""
|
|
303
444
|
|
|
445
|
+
def get_categories(self) -> list[str]:
|
|
446
|
+
if self._enabled:
|
|
447
|
+
return ["binary_sensor", "led"]
|
|
448
|
+
return []
|
|
449
|
+
|
|
304
450
|
|
|
305
451
|
class ThermostatChannel(Button):
|
|
306
452
|
"""
|
|
307
453
|
A Thermostat channel
|
|
308
454
|
These are the booster/heater/alarms
|
|
309
|
-
HASS OK
|
|
310
455
|
"""
|
|
311
456
|
|
|
312
457
|
|
|
313
458
|
class Dimmer(Channel):
|
|
314
459
|
"""
|
|
315
460
|
A Dimmer channel
|
|
316
|
-
HASS OK
|
|
317
461
|
"""
|
|
318
462
|
|
|
319
463
|
_state: int = 0
|
|
320
464
|
|
|
321
|
-
def
|
|
465
|
+
def __init__(
|
|
466
|
+
self,
|
|
467
|
+
module: Module,
|
|
468
|
+
num: int,
|
|
469
|
+
name: str,
|
|
470
|
+
nameEditable: bool,
|
|
471
|
+
subDevice: bool,
|
|
472
|
+
writer: Callable[[Message], Awaitable[None]],
|
|
473
|
+
address: int,
|
|
474
|
+
slider_scale: int = 100,
|
|
475
|
+
):
|
|
476
|
+
super().__init__(module, num, name, nameEditable, subDevice, writer, address)
|
|
477
|
+
|
|
478
|
+
self.slider_scale = slider_scale
|
|
479
|
+
# VMB4DC has dim values 0(off), 1-99(dimmed), 100(full on)
|
|
480
|
+
# VMBDALI has dim values 0(off), 1-253(dimmed), 254(full on), 255(previous value)
|
|
481
|
+
|
|
482
|
+
def get_categories(self) -> list[str]:
|
|
322
483
|
return ["light"]
|
|
323
484
|
|
|
324
485
|
def is_on(self) -> bool:
|
|
@@ -333,20 +494,20 @@ class Dimmer(Channel):
|
|
|
333
494
|
"""
|
|
334
495
|
Return the dimmer state
|
|
335
496
|
"""
|
|
336
|
-
return self._state
|
|
497
|
+
return int(self._state * 100 / self.slider_scale)
|
|
337
498
|
|
|
338
|
-
async def set_dimmer_state(self, slider, transitiontime=0) -> None:
|
|
499
|
+
async def set_dimmer_state(self, slider: int, transitiontime: int = 0) -> None:
|
|
339
500
|
"""
|
|
340
501
|
Set dimmer to slider
|
|
341
502
|
"""
|
|
342
503
|
cls = commandRegistry.get_command(0x07, self._module.get_type())
|
|
343
504
|
msg = cls(self._address)
|
|
344
|
-
msg.dimmer_state = slider
|
|
505
|
+
msg.dimmer_state = int(slider * self.slider_scale / 100)
|
|
345
506
|
msg.dimmer_transitiontime = int(transitiontime)
|
|
346
507
|
msg.dimmer_channels = [self._num]
|
|
347
508
|
await self._writer(msg)
|
|
348
509
|
|
|
349
|
-
async def restore_dimmer_state(self, transitiontime=0) -> None:
|
|
510
|
+
async def restore_dimmer_state(self, transitiontime: int = 0) -> None:
|
|
350
511
|
"""
|
|
351
512
|
restore dimmer to last known state
|
|
352
513
|
"""
|
|
@@ -360,14 +521,22 @@ class Dimmer(Channel):
|
|
|
360
521
|
class Temperature(Channel):
|
|
361
522
|
"""
|
|
362
523
|
A Temperature sensor channel
|
|
363
|
-
HASS OK
|
|
364
524
|
"""
|
|
365
525
|
|
|
366
526
|
_cur = 0
|
|
527
|
+
_cur_precision = None
|
|
367
528
|
_max = None
|
|
368
529
|
_min = None
|
|
369
|
-
|
|
370
|
-
|
|
530
|
+
_target = 0
|
|
531
|
+
_cmode = None
|
|
532
|
+
_cool_mode = None
|
|
533
|
+
_cstatus = None
|
|
534
|
+
_thermostat = False
|
|
535
|
+
_sleep_timer = 0
|
|
536
|
+
|
|
537
|
+
def get_categories(self) -> list[str]:
|
|
538
|
+
if self._thermostat:
|
|
539
|
+
return ["sensor", "climate"]
|
|
371
540
|
return ["sensor"]
|
|
372
541
|
|
|
373
542
|
def get_class(self) -> str:
|
|
@@ -379,59 +548,169 @@ class Temperature(Channel):
|
|
|
379
548
|
def get_state(self) -> int:
|
|
380
549
|
return round(self._cur, 2)
|
|
381
550
|
|
|
551
|
+
def is_temperature(self) -> bool:
|
|
552
|
+
return True
|
|
553
|
+
|
|
554
|
+
def get_max(self) -> int | None:
|
|
555
|
+
if self._max is None:
|
|
556
|
+
return None
|
|
557
|
+
return round(self._max, 2)
|
|
558
|
+
|
|
559
|
+
def get_min(self) -> int | None:
|
|
560
|
+
if self._min is None:
|
|
561
|
+
return None
|
|
562
|
+
return round(self._min, 2)
|
|
563
|
+
|
|
564
|
+
def get_climate_target(self) -> int:
|
|
565
|
+
return round(self._target, 2)
|
|
566
|
+
|
|
567
|
+
def get_climate_preset(self) -> str:
|
|
568
|
+
return self._cmode
|
|
569
|
+
|
|
570
|
+
def get_climate_mode(self) -> str:
|
|
571
|
+
return self._cstatus
|
|
572
|
+
|
|
573
|
+
def get_cool_mode(self) -> str:
|
|
574
|
+
return self._cool_mode
|
|
575
|
+
|
|
576
|
+
async def set_temp(self, temp: float) -> None:
|
|
577
|
+
cls = commandRegistry.get_command(0xE4, self._module.get_type())
|
|
578
|
+
msg = cls(self._address)
|
|
579
|
+
msg.temp = temp * 2 # TODO: int()
|
|
580
|
+
await self._writer(msg)
|
|
581
|
+
|
|
582
|
+
async def _switch_mode(self) -> None:
|
|
583
|
+
if self._cmode == "safe":
|
|
584
|
+
code = 0xDE
|
|
585
|
+
elif self._cmode == "comfort":
|
|
586
|
+
code = 0xDB
|
|
587
|
+
elif self._cmode == "day":
|
|
588
|
+
code = 0xDC
|
|
589
|
+
else: # "night"
|
|
590
|
+
code = 0xDD
|
|
591
|
+
|
|
592
|
+
if self._cstatus == "run":
|
|
593
|
+
sleep = 0x0
|
|
594
|
+
elif self._cstatus == "manual":
|
|
595
|
+
sleep = 0xFFFF
|
|
596
|
+
elif self._cstatus == "sleep":
|
|
597
|
+
sleep = self._sleep_timer
|
|
598
|
+
else:
|
|
599
|
+
sleep = 0x0
|
|
600
|
+
cls = commandRegistry.get_command(code, self._module.get_type())
|
|
601
|
+
msg = cls(self._address, sleep)
|
|
602
|
+
await self._writer(msg)
|
|
603
|
+
|
|
604
|
+
async def set_preset(self, preset: str) -> None:
|
|
605
|
+
self._cmode = preset
|
|
606
|
+
await self._switch_mode()
|
|
607
|
+
|
|
608
|
+
async def set_climate_mode(self, mode: str) -> None:
|
|
609
|
+
self._cstatus = mode
|
|
610
|
+
await self._switch_mode()
|
|
611
|
+
|
|
612
|
+
async def set_mode(self, mode: str) -> None:
|
|
613
|
+
# TODO: change function name, proposal = set_heat_cool_mode
|
|
614
|
+
if mode == "heat":
|
|
615
|
+
code = 0xE0
|
|
616
|
+
elif mode == "cool":
|
|
617
|
+
code = 0xDF
|
|
618
|
+
# TODO: else case
|
|
619
|
+
cls = commandRegistry.get_command(code, self._module.get_type())
|
|
620
|
+
msg = cls(self._address)
|
|
621
|
+
await self._writer(msg)
|
|
622
|
+
|
|
623
|
+
async def maybe_update_temperature(self, new_temp: float, precision: float) -> None:
|
|
624
|
+
# Based on experiments, Velbus modules seem to truncate (i.e. round down)
|
|
625
|
+
current_temp_rounded_to_precision = (
|
|
626
|
+
math.floor(self._cur / precision) * precision
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
if current_temp_rounded_to_precision == new_temp:
|
|
630
|
+
# The newly received temperature is still in line with our current value,
|
|
631
|
+
# but with reduced precision.
|
|
632
|
+
# Don't update (would lose high precision)
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
elif (
|
|
636
|
+
current_temp_rounded_to_precision - precision
|
|
637
|
+
<= new_temp
|
|
638
|
+
< current_temp_rounded_to_precision
|
|
639
|
+
and self._cur_precision < precision
|
|
640
|
+
):
|
|
641
|
+
# The newly received temperature is 1 LSb below the current value
|
|
642
|
+
# and the current value was set by a better precision message
|
|
643
|
+
# Modify the received temperature by "adding precision", while still keeping the same low precision value
|
|
644
|
+
# e.g. (decimal digits represent precision)
|
|
645
|
+
# | Actual | Msg | Stored |
|
|
646
|
+
# | 21.0000 | 21.0000 | 21.0000 |
|
|
647
|
+
# | 20.9375 | 20.5 | 20.9375 |
|
|
648
|
+
new_temp = current_temp_rounded_to_precision - self._cur_precision
|
|
649
|
+
|
|
650
|
+
await self.update(
|
|
651
|
+
{
|
|
652
|
+
"cur": new_temp,
|
|
653
|
+
"cur_precision": precision,
|
|
654
|
+
}
|
|
655
|
+
)
|
|
656
|
+
|
|
382
657
|
|
|
383
658
|
class SensorNumber(Channel):
|
|
384
659
|
"""
|
|
385
660
|
A Numeric Sensor channel
|
|
386
|
-
HASS OK
|
|
387
661
|
"""
|
|
388
662
|
|
|
389
663
|
_cur = 0
|
|
664
|
+
_unit = None
|
|
390
665
|
|
|
391
|
-
def get_categories(self):
|
|
666
|
+
def get_categories(self) -> list[str]:
|
|
392
667
|
return ["sensor"]
|
|
393
668
|
|
|
394
|
-
def get_class(self):
|
|
669
|
+
def get_class(self) -> None:
|
|
395
670
|
return None
|
|
396
671
|
|
|
397
|
-
def get_unit(self):
|
|
398
|
-
return
|
|
672
|
+
def get_unit(self) -> None:
|
|
673
|
+
return self._unit
|
|
399
674
|
|
|
400
|
-
def get_state(self):
|
|
675
|
+
def get_state(self) -> float:
|
|
401
676
|
return round(self._cur, 2)
|
|
402
677
|
|
|
403
678
|
|
|
404
679
|
class LightSensor(Channel):
|
|
405
680
|
"""
|
|
406
681
|
A light sensor channel
|
|
407
|
-
HASS OK
|
|
408
682
|
"""
|
|
409
683
|
|
|
410
684
|
_cur = 0
|
|
411
685
|
|
|
412
|
-
def get_categories(self):
|
|
686
|
+
def get_categories(self) -> list[str]:
|
|
413
687
|
return ["sensor"]
|
|
414
688
|
|
|
415
|
-
def get_class(self):
|
|
689
|
+
def get_class(self) -> str:
|
|
416
690
|
return DEVICE_CLASS_ILLUMINANCE
|
|
417
691
|
|
|
418
|
-
def get_unit(self):
|
|
692
|
+
def get_unit(self) -> None:
|
|
419
693
|
return None
|
|
420
694
|
|
|
421
|
-
def get_state(self):
|
|
695
|
+
def get_state(self) -> float:
|
|
422
696
|
return round(self._cur, 2)
|
|
423
697
|
|
|
424
698
|
|
|
425
699
|
class Relay(Channel):
|
|
426
700
|
"""
|
|
427
701
|
A Relay channel
|
|
428
|
-
HASS OK
|
|
429
702
|
"""
|
|
430
703
|
|
|
431
704
|
_on = None
|
|
705
|
+
_enabled = True
|
|
706
|
+
_inhibit = False
|
|
707
|
+
_forced_on = False
|
|
708
|
+
_disabled = False
|
|
432
709
|
|
|
433
|
-
def get_categories(self) -> list:
|
|
434
|
-
|
|
710
|
+
def get_categories(self) -> list[str]:
|
|
711
|
+
if self._enabled:
|
|
712
|
+
return ["switch"]
|
|
713
|
+
return []
|
|
435
714
|
|
|
436
715
|
def is_on(self) -> bool:
|
|
437
716
|
"""
|
|
@@ -439,6 +718,15 @@ class Relay(Channel):
|
|
|
439
718
|
"""
|
|
440
719
|
return self._on
|
|
441
720
|
|
|
721
|
+
def is_inhibit(self) -> bool:
|
|
722
|
+
return self._inhibit
|
|
723
|
+
|
|
724
|
+
def is_forced_on(self) -> bool:
|
|
725
|
+
return self._forced_on
|
|
726
|
+
|
|
727
|
+
def is_disabled(self) -> bool:
|
|
728
|
+
return self._disabled
|
|
729
|
+
|
|
442
730
|
async def turn_on(self) -> None:
|
|
443
731
|
"""
|
|
444
732
|
Send the turn on message
|
|
@@ -463,8 +751,42 @@ class EdgeLit(Channel):
|
|
|
463
751
|
An EdgeLit channel
|
|
464
752
|
"""
|
|
465
753
|
|
|
466
|
-
|
|
467
|
-
|
|
754
|
+
async def reset_color(self, left=True, top=True, right=True, bottom=True):
|
|
755
|
+
msg = SetEdgeColorMessage(self._address)
|
|
756
|
+
msg.apply_background_color = True
|
|
757
|
+
msg.color_idx = 0
|
|
758
|
+
msg.apply_to_left_edge = left
|
|
759
|
+
msg.apply_to_top_edge = top
|
|
760
|
+
msg.apply_to_right_edge = right
|
|
761
|
+
msg.apply_to_bottom_edge = bottom
|
|
762
|
+
msg.apply_to_all_pages = True
|
|
763
|
+
await self._writer(msg)
|
|
764
|
+
|
|
765
|
+
async def set_color(
|
|
766
|
+
self,
|
|
767
|
+
color_idx: int,
|
|
768
|
+
left=True,
|
|
769
|
+
top=True,
|
|
770
|
+
right=True,
|
|
771
|
+
bottom=True,
|
|
772
|
+
blinking=False,
|
|
773
|
+
priority=CustomColorPriority.LOW_PRIORITY,
|
|
774
|
+
) -> None:
|
|
775
|
+
"""
|
|
776
|
+
Send the turn off message
|
|
777
|
+
"""
|
|
778
|
+
|
|
779
|
+
msg = SetEdgeColorMessage(self._address)
|
|
780
|
+
msg.apply_background_color = True
|
|
781
|
+
msg.background_blinking = blinking
|
|
782
|
+
msg.color_idx = color_idx
|
|
783
|
+
msg.apply_to_left_edge = left
|
|
784
|
+
msg.apply_to_top_edge = top
|
|
785
|
+
msg.apply_to_right_edge = right
|
|
786
|
+
msg.apply_to_bottom_edge = bottom
|
|
787
|
+
msg.apply_to_all_pages = True
|
|
788
|
+
msg.custom_color_priority = priority
|
|
789
|
+
await self._writer(msg)
|
|
468
790
|
|
|
469
791
|
|
|
470
792
|
class Memo(Channel):
|
|
@@ -472,32 +794,44 @@ class Memo(Channel):
|
|
|
472
794
|
A Memo text
|
|
473
795
|
"""
|
|
474
796
|
|
|
797
|
+
async def set(self, txt: str) -> None:
|
|
798
|
+
cls = commandRegistry.get_command(0xAC, self._module.get_type())
|
|
799
|
+
msg = cls(self._address)
|
|
800
|
+
msgcntr = 0
|
|
801
|
+
for char in txt:
|
|
802
|
+
msg.memo_text += char
|
|
803
|
+
if len(msg.memo_text) >= 5:
|
|
804
|
+
msgcntr += 5
|
|
805
|
+
await self._writer(msg)
|
|
806
|
+
msg = cls(self._address)
|
|
807
|
+
msg.start = msgcntr
|
|
808
|
+
await self._writer(msg)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
class SelectedProgram(Channel):
|
|
812
|
+
"""
|
|
813
|
+
A selected program channel
|
|
814
|
+
"""
|
|
815
|
+
|
|
816
|
+
_selected_program_str = None
|
|
475
817
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
# elif mode == "night":
|
|
497
|
-
# code = 0xDD
|
|
498
|
-
# cls = commandRegistry.get_command(code, self._module.get_type())
|
|
499
|
-
# msg = cls(self._address)
|
|
500
|
-
# await self._writer(msg)
|
|
501
|
-
#
|
|
502
|
-
# def get_state(self) -> int:
|
|
503
|
-
# return round(self._cur, 2)
|
|
818
|
+
def get_categories(self) -> list[str]:
|
|
819
|
+
return ["select"]
|
|
820
|
+
|
|
821
|
+
def get_class(self) -> None:
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
def get_options(self) -> list:
|
|
825
|
+
return list(PROGRAM_SELECTION.values())
|
|
826
|
+
|
|
827
|
+
def get_selected_program(self) -> str:
|
|
828
|
+
return self._selected_program_str
|
|
829
|
+
|
|
830
|
+
async def set_selected_program(self, program_str: str) -> None:
|
|
831
|
+
self._selected_program_str = program_str
|
|
832
|
+
command_code = 0xB3
|
|
833
|
+
cls = commandRegistry.get_command(command_code, self._module.get_type())
|
|
834
|
+
index = list(PROGRAM_SELECTION.values()).index(program_str)
|
|
835
|
+
program = list(PROGRAM_SELECTION.keys())[index]
|
|
836
|
+
msg = cls(self._address, program)
|
|
837
|
+
await self._writer(msg)
|