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/module.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This represents a velbus module
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
|
|
7
|
+
import asyncio
|
|
8
|
+
import importlib.resources
|
|
9
|
+
import json
|
|
6
10
|
import logging
|
|
7
11
|
import os
|
|
8
12
|
import pathlib
|
|
9
|
-
import pickle
|
|
10
13
|
import struct
|
|
11
14
|
import sys
|
|
12
15
|
from typing import Awaitable, Callable
|
|
13
16
|
|
|
17
|
+
from aiofile import async_open
|
|
18
|
+
|
|
14
19
|
from velbusaio.channels import (
|
|
15
20
|
Blind,
|
|
16
21
|
Button,
|
|
@@ -24,7 +29,10 @@ from velbusaio.channels import (
|
|
|
24
29
|
SelectedProgram,
|
|
25
30
|
Sensor,
|
|
26
31
|
SensorNumber,
|
|
27
|
-
|
|
32
|
+
)
|
|
33
|
+
from velbusaio.channels import Temperature
|
|
34
|
+
from velbusaio.channels import Temperature as TemperatureChannelType
|
|
35
|
+
from velbusaio.channels import (
|
|
28
36
|
ThermostatChannel,
|
|
29
37
|
)
|
|
30
38
|
from velbusaio.command_registry import commandRegistry
|
|
@@ -33,10 +41,10 @@ from velbusaio.const import (
|
|
|
33
41
|
CHANNEL_MEMO_TEXT,
|
|
34
42
|
CHANNEL_SELECTED_PROGRAM,
|
|
35
43
|
PRIORITY_LOW,
|
|
44
|
+
SCAN_MODULEINFO_TIMEOUT_INITIAL,
|
|
36
45
|
)
|
|
37
|
-
from velbusaio.helpers import handle_match, keys_exists
|
|
46
|
+
from velbusaio.helpers import h2, handle_match, keys_exists
|
|
38
47
|
from velbusaio.message import Message
|
|
39
|
-
from velbusaio.messages import DaliDeviceSettingMsg
|
|
40
48
|
from velbusaio.messages.blind_status import BlindStatusMessage, BlindStatusNgMessage
|
|
41
49
|
from velbusaio.messages.channel_name_part1 import (
|
|
42
50
|
ChannelNamePart1Message,
|
|
@@ -56,23 +64,28 @@ from velbusaio.messages.channel_name_part3 import (
|
|
|
56
64
|
from velbusaio.messages.channel_name_request import (
|
|
57
65
|
COMMAND_CODE as CHANNEL_NAME_REQUEST_COMMAND_CODE,
|
|
58
66
|
)
|
|
59
|
-
from velbusaio.messages.channel_name_request import
|
|
67
|
+
from velbusaio.messages.channel_name_request import (
|
|
68
|
+
ChannelNameRequestMessage,
|
|
69
|
+
)
|
|
60
70
|
from velbusaio.messages.clear_led import ClearLedMessage
|
|
61
71
|
from velbusaio.messages.counter_status import CounterStatusMessage
|
|
62
72
|
from velbusaio.messages.counter_status_request import CounterStatusRequestMessage
|
|
73
|
+
from velbusaio.messages.counter_value import CounterValueMessage
|
|
74
|
+
from velbusaio.messages.dali_device_settings import DaliDeviceSettingMsg
|
|
63
75
|
from velbusaio.messages.dali_device_settings import DeviceType as DaliDeviceType
|
|
64
76
|
from velbusaio.messages.dali_device_settings import DeviceTypeMsg as DaliDeviceTypeMsg
|
|
65
77
|
from velbusaio.messages.dali_device_settings import MemberOfGroupMsg
|
|
66
78
|
from velbusaio.messages.dali_device_settings_request import (
|
|
67
79
|
COMMAND_CODE as DALI_DEVICE_SETTINGS_REQUEST_COMMAND_CODE,
|
|
68
80
|
)
|
|
69
|
-
from velbusaio.messages.dali_device_settings_request import
|
|
81
|
+
from velbusaio.messages.dali_device_settings_request import (
|
|
82
|
+
DaliDeviceSettingsRequest,
|
|
83
|
+
)
|
|
70
84
|
from velbusaio.messages.dali_dim_value_status import DimValueStatus
|
|
71
85
|
from velbusaio.messages.dimmer_channel_status import DimmerChannelStatusMessage
|
|
72
86
|
from velbusaio.messages.dimmer_status import DimmerStatusMessage
|
|
73
87
|
from velbusaio.messages.fast_blinking_led import FastBlinkingLedMessage
|
|
74
88
|
from velbusaio.messages.memory_data import MemoryDataMessage
|
|
75
|
-
from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
|
|
76
89
|
from velbusaio.messages.module_status import (
|
|
77
90
|
ModuleStatusGP4PirMessage,
|
|
78
91
|
ModuleStatusMessage,
|
|
@@ -80,9 +93,8 @@ from velbusaio.messages.module_status import (
|
|
|
80
93
|
ModuleStatusPirMessage,
|
|
81
94
|
)
|
|
82
95
|
from velbusaio.messages.module_status_request import ModuleStatusRequestMessage
|
|
83
|
-
from velbusaio.messages.module_subtype import ModuleSubTypeMessage
|
|
84
|
-
from velbusaio.messages.module_type import ModuleTypeMessage, ModuleType2Message
|
|
85
96
|
from velbusaio.messages.push_button_status import PushButtonStatusMessage
|
|
97
|
+
from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
|
|
86
98
|
from velbusaio.messages.read_data_from_memory import ReadDataFromMemoryMessage
|
|
87
99
|
from velbusaio.messages.relay_status import RelayStatusMessage, RelayStatusMessage2
|
|
88
100
|
from velbusaio.messages.sensor_temperature import SensorTemperatureMessage
|
|
@@ -91,7 +103,6 @@ from velbusaio.messages.slider_status import SliderStatusMessage
|
|
|
91
103
|
from velbusaio.messages.slow_blinking_led import SlowBlinkingLedMessage
|
|
92
104
|
from velbusaio.messages.temp_sensor_status import TempSensorStatusMessage
|
|
93
105
|
from velbusaio.messages.update_led_status import UpdateLedStatusMessage
|
|
94
|
-
from velbusaio.channels import Temperature as TemperatureChannelType
|
|
95
106
|
|
|
96
107
|
|
|
97
108
|
class Module:
|
|
@@ -104,18 +115,16 @@ class Module:
|
|
|
104
115
|
cls,
|
|
105
116
|
module_address: int,
|
|
106
117
|
module_type: int,
|
|
107
|
-
module_data: dict,
|
|
108
118
|
serial: int | None = None,
|
|
109
119
|
memorymap: int | None = None,
|
|
110
120
|
build_year: int | None = None,
|
|
111
121
|
build_week: int | None = None,
|
|
112
122
|
cache_dir: str | None = None,
|
|
113
123
|
) -> Module:
|
|
114
|
-
if module_type == 0x45:
|
|
124
|
+
if module_type == 0x45 or module_type == 0x5A:
|
|
115
125
|
return VmbDali(
|
|
116
126
|
module_address,
|
|
117
127
|
module_type,
|
|
118
|
-
module_data,
|
|
119
128
|
serial,
|
|
120
129
|
memorymap,
|
|
121
130
|
build_year,
|
|
@@ -126,7 +135,6 @@ class Module:
|
|
|
126
135
|
return Module(
|
|
127
136
|
module_address,
|
|
128
137
|
module_type,
|
|
129
|
-
module_data,
|
|
130
138
|
serial,
|
|
131
139
|
memorymap,
|
|
132
140
|
build_year,
|
|
@@ -138,7 +146,6 @@ class Module:
|
|
|
138
146
|
self,
|
|
139
147
|
module_address: int,
|
|
140
148
|
module_type: int,
|
|
141
|
-
module_data: dict,
|
|
142
149
|
serial: int | None = None,
|
|
143
150
|
memorymap: int | None = None,
|
|
144
151
|
build_year: int | None = None,
|
|
@@ -146,8 +153,8 @@ class Module:
|
|
|
146
153
|
cache_dir: str | None = None,
|
|
147
154
|
) -> None:
|
|
148
155
|
self._address = module_address
|
|
149
|
-
self._type = module_type
|
|
150
|
-
self._data =
|
|
156
|
+
self._type = int(module_type)
|
|
157
|
+
self._data = {}
|
|
151
158
|
|
|
152
159
|
self._name = {}
|
|
153
160
|
self._sub_address = {}
|
|
@@ -157,11 +164,47 @@ class Module:
|
|
|
157
164
|
self.build_week = build_week
|
|
158
165
|
self._cache_dir = cache_dir
|
|
159
166
|
self._is_loading = False
|
|
167
|
+
self._got_status = asyncio.Event()
|
|
168
|
+
self._got_status.clear()
|
|
160
169
|
self._channels = {}
|
|
161
170
|
self.loaded = False
|
|
171
|
+
self._use_cache = True
|
|
172
|
+
self._loaded_cache = {}
|
|
173
|
+
|
|
174
|
+
async def wait_for_status_messages(self) -> None:
|
|
175
|
+
try:
|
|
176
|
+
await asyncio.wait_for(self._got_status.wait(), 2)
|
|
177
|
+
except Exception:
|
|
178
|
+
self._log.warning(f"Timeout waiting for status messages for: {self}")
|
|
162
179
|
|
|
163
|
-
def
|
|
180
|
+
def get_initial_timeout(self) -> int:
|
|
181
|
+
return SCAN_MODULEINFO_TIMEOUT_INITIAL
|
|
182
|
+
|
|
183
|
+
async def initialize(self, writer: Callable[[Message], Awaitable[None]]) -> None:
|
|
164
184
|
self._log = logging.getLogger("velbus-module")
|
|
185
|
+
# load the protocol data
|
|
186
|
+
try:
|
|
187
|
+
if sys.version_info >= (3, 13):
|
|
188
|
+
with importlib.resources.path(
|
|
189
|
+
__name__, f"module_spec/{h2(self._type)}.json"
|
|
190
|
+
) as fspath:
|
|
191
|
+
async with async_open(fspath) as protocol_file:
|
|
192
|
+
self._data = json.loads(await protocol_file.read())
|
|
193
|
+
else:
|
|
194
|
+
async with async_open(
|
|
195
|
+
str(
|
|
196
|
+
importlib.resources.files(__name__.split(".")[0]).joinpath(
|
|
197
|
+
f"module_spec/{h2(self._type)}.json"
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
) as protocol_file:
|
|
201
|
+
self._data = json.loads(await protocol_file.read())
|
|
202
|
+
self._log.debug(f"Module spec {h2(self._type)} loaded")
|
|
203
|
+
except FileNotFoundError:
|
|
204
|
+
self._log.warning(f"No module spec for {h2(self._type)}")
|
|
205
|
+
self._data = {}
|
|
206
|
+
|
|
207
|
+
# set some params from the velbus controller
|
|
165
208
|
self._writer = writer
|
|
166
209
|
for chan in self._channels.values():
|
|
167
210
|
chan._writer = writer
|
|
@@ -186,10 +229,12 @@ class Module:
|
|
|
186
229
|
):
|
|
187
230
|
del self._channels[i]
|
|
188
231
|
|
|
189
|
-
def _cache(self) -> None:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
232
|
+
async def _cache(self) -> None:
|
|
233
|
+
if not self._use_cache:
|
|
234
|
+
return
|
|
235
|
+
cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.json")
|
|
236
|
+
async with async_open(cfile, "w") as fl:
|
|
237
|
+
await fl.write(json.dumps(self.to_cache(), indent=4))
|
|
193
238
|
|
|
194
239
|
def __getstate__(self) -> dict:
|
|
195
240
|
d = self.__dict__
|
|
@@ -200,20 +245,20 @@ class Module:
|
|
|
200
245
|
self.__dict__ = state
|
|
201
246
|
|
|
202
247
|
def __repr__(self) -> str:
|
|
203
|
-
return
|
|
204
|
-
"<{}: {{{}}} @ {{{}}} loaded:{{{}}} loading:{{{}}} channels{{:{}}}>".format(
|
|
205
|
-
self._name,
|
|
206
|
-
self._type,
|
|
207
|
-
self._address,
|
|
208
|
-
self.loaded,
|
|
209
|
-
self._is_loading,
|
|
210
|
-
self._channels,
|
|
211
|
-
)
|
|
212
|
-
)
|
|
248
|
+
return f"<{self._name} type:{self._type} address:{self._address} loaded:{self.loaded} loading:{self._is_loading} channels: {self._channels}>"
|
|
213
249
|
|
|
214
250
|
def __str__(self) -> str:
|
|
215
251
|
return self.__repr__()
|
|
216
252
|
|
|
253
|
+
def to_cache(self) -> dict:
|
|
254
|
+
d = {"name": self._name, "channels": {}}
|
|
255
|
+
for num, chan in self._channels.items():
|
|
256
|
+
d["channels"][num] = chan.to_cache()
|
|
257
|
+
return d
|
|
258
|
+
|
|
259
|
+
def get_address(self) -> int:
|
|
260
|
+
return self._address
|
|
261
|
+
|
|
217
262
|
def get_addresses(self) -> list:
|
|
218
263
|
"""
|
|
219
264
|
Get all addresses for this module
|
|
@@ -231,7 +276,9 @@ class Module:
|
|
|
231
276
|
return self._type
|
|
232
277
|
|
|
233
278
|
def get_type_name(self) -> str:
|
|
234
|
-
|
|
279
|
+
if "Type" in self._data:
|
|
280
|
+
return self._data["Type"]
|
|
281
|
+
return "UNKNOWN"
|
|
235
282
|
|
|
236
283
|
def get_serial(self) -> str | None:
|
|
237
284
|
return self.serial
|
|
@@ -240,12 +287,7 @@ class Module:
|
|
|
240
287
|
return self._name
|
|
241
288
|
|
|
242
289
|
def get_sw_version(self) -> str:
|
|
243
|
-
return "{}-{}.{}.{}"
|
|
244
|
-
self.serial,
|
|
245
|
-
self.memory_map_version,
|
|
246
|
-
self.build_year,
|
|
247
|
-
self.build_week,
|
|
248
|
-
)
|
|
290
|
+
return f"{self.serial}-{self.memory_map_version}.{self.build_year}.{self.build_week}"
|
|
249
291
|
|
|
250
292
|
def calc_channel_offset(self, address: int) -> int:
|
|
251
293
|
_channel_offset = 0
|
|
@@ -260,6 +302,7 @@ class Module:
|
|
|
260
302
|
"""
|
|
261
303
|
Process received message
|
|
262
304
|
"""
|
|
305
|
+
self._log.debug(f"RX: {message}")
|
|
263
306
|
_channel_offset = self.calc_channel_offset(message.address)
|
|
264
307
|
|
|
265
308
|
if isinstance(
|
|
@@ -271,6 +314,7 @@ class Module:
|
|
|
271
314
|
),
|
|
272
315
|
):
|
|
273
316
|
self._process_channel_name_message(1, message)
|
|
317
|
+
await self._cache()
|
|
274
318
|
elif isinstance(
|
|
275
319
|
message,
|
|
276
320
|
(
|
|
@@ -280,6 +324,7 @@ class Module:
|
|
|
280
324
|
),
|
|
281
325
|
):
|
|
282
326
|
self._process_channel_name_message(2, message)
|
|
327
|
+
await self._cache()
|
|
283
328
|
elif isinstance(
|
|
284
329
|
message,
|
|
285
330
|
(
|
|
@@ -289,6 +334,7 @@ class Module:
|
|
|
289
334
|
),
|
|
290
335
|
):
|
|
291
336
|
self._process_channel_name_message(3, message)
|
|
337
|
+
await self._cache()
|
|
292
338
|
elif isinstance(message, MemoryDataMessage):
|
|
293
339
|
await self._process_memory_data_message(message)
|
|
294
340
|
elif isinstance(message, (RelayStatusMessage, RelayStatusMessage2)):
|
|
@@ -516,14 +562,22 @@ class Module:
|
|
|
516
562
|
await self._update_channel(
|
|
517
563
|
message.sensor, {"cur": message.value, "unit": message.unit}
|
|
518
564
|
)
|
|
519
|
-
|
|
520
|
-
|
|
565
|
+
elif isinstance(message, CounterValueMessage):
|
|
566
|
+
await self._update_channel(
|
|
567
|
+
message.channel, {"power": message.power, "energy": message.energy}
|
|
568
|
+
)
|
|
569
|
+
elif isinstance(message, DimValueStatus):
|
|
570
|
+
for offset, dim_value in enumerate(message.dim_values):
|
|
571
|
+
channel = message.channel + offset
|
|
572
|
+
await self._update_channel(channel, {"state": dim_value})
|
|
573
|
+
# notigy status
|
|
574
|
+
self._got_status.set()
|
|
521
575
|
|
|
522
576
|
async def _update_channel(self, channel: int, updates: dict):
|
|
523
577
|
try:
|
|
524
578
|
await self._channels[channel].update(updates)
|
|
525
579
|
except KeyError:
|
|
526
|
-
self._log.
|
|
580
|
+
self._log.info(
|
|
527
581
|
f"channel {channel} does not exist for module @ address {self}"
|
|
528
582
|
)
|
|
529
583
|
|
|
@@ -533,33 +587,61 @@ class Module:
|
|
|
533
587
|
"""
|
|
534
588
|
return self._channels
|
|
535
589
|
|
|
590
|
+
async def load_from_vlp(self, vlp_data: dict) -> None:
|
|
591
|
+
self._name = vlp_data.get_name()
|
|
592
|
+
self._data["Channels"] = vlp_data.get_channels()
|
|
593
|
+
self._use_cache = False
|
|
594
|
+
self._is_loading = False
|
|
595
|
+
self.loaded = True
|
|
596
|
+
await self._load_default_channels()
|
|
597
|
+
# TODO set all channels to _is_loaded = True
|
|
598
|
+
for chan in self._channels.values():
|
|
599
|
+
chan._is_loaded = True
|
|
600
|
+
await self._request_module_status()
|
|
601
|
+
|
|
536
602
|
async def load(self, from_cache: bool = False) -> None:
|
|
537
|
-
"""
|
|
538
|
-
Retrieve names of channels
|
|
539
|
-
"""
|
|
540
|
-
# did we already start the loading?
|
|
541
|
-
# this is needed for the submodules,
|
|
542
|
-
# as the submodule address maps to the main module
|
|
543
|
-
# this method can be called multiple times
|
|
544
|
-
if self._is_loading or self.loaded:
|
|
545
|
-
if from_cache:
|
|
546
|
-
await self._request_module_status()
|
|
547
|
-
return
|
|
548
|
-
self._log.info("Load Module")
|
|
549
603
|
# start the loading
|
|
550
604
|
self._is_loading = True
|
|
605
|
+
# see if we have a cache
|
|
606
|
+
cache = await self._get_cache()
|
|
607
|
+
self._loaded_cache = cache
|
|
551
608
|
# load default channels
|
|
552
|
-
await self.
|
|
609
|
+
await self._load_default_channels()
|
|
610
|
+
|
|
553
611
|
# load the data from memory ( the stuff that we need)
|
|
554
|
-
|
|
612
|
+
if "name" in cache and cache["name"] != "":
|
|
613
|
+
self._name = cache["name"]
|
|
614
|
+
else:
|
|
615
|
+
await self.__load_memory()
|
|
555
616
|
# load the module status
|
|
556
|
-
await self._request_module_status()
|
|
617
|
+
# await self._request_module_status()
|
|
557
618
|
# load the channel names
|
|
558
|
-
|
|
619
|
+
if "channels" in cache:
|
|
620
|
+
for num, chan in cache["channels"].items():
|
|
621
|
+
self._channels[int(num)]._name = chan["name"]
|
|
622
|
+
if "subdevice" in chan:
|
|
623
|
+
self._channels[int(num)]._sub_device = chan["subdevice"]
|
|
624
|
+
else:
|
|
625
|
+
self._channels[int(num)]._sub_device = False
|
|
626
|
+
if "Unit" in chan:
|
|
627
|
+
self._channels[int(num)]._Unit = chan["Unit"]
|
|
628
|
+
self._channels[int(num)]._is_loaded = True
|
|
629
|
+
else:
|
|
630
|
+
await self._request_channel_name()
|
|
559
631
|
# load the module specific stuff
|
|
560
632
|
self._load()
|
|
561
633
|
# stop the loading
|
|
562
634
|
self._is_loading = False
|
|
635
|
+
await self._request_module_status()
|
|
636
|
+
|
|
637
|
+
async def _get_cache(self):
|
|
638
|
+
try:
|
|
639
|
+
cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.json")
|
|
640
|
+
async with async_open(cfile, "r") as fl:
|
|
641
|
+
cache = json.loads(await fl.read())
|
|
642
|
+
except OSError:
|
|
643
|
+
cache = {}
|
|
644
|
+
return cache
|
|
563
645
|
|
|
564
646
|
def _load(self) -> None:
|
|
565
647
|
"""
|
|
@@ -586,7 +668,11 @@ class Module:
|
|
|
586
668
|
addr = "{high:02X}{low:02X}".format(
|
|
587
669
|
high=message.high_address, low=message.low_address
|
|
588
670
|
)
|
|
589
|
-
|
|
671
|
+
if "Memory" not in self._data:
|
|
672
|
+
return
|
|
673
|
+
if "Address" not in self._data["Memory"]:
|
|
674
|
+
return
|
|
675
|
+
mdata = self._data["Memory"]["Address"][addr]
|
|
590
676
|
if "ModuleName" in mdata and isinstance(self._name, dict):
|
|
591
677
|
# if self._name is a dict we are still loading
|
|
592
678
|
# if its a string it was already complete
|
|
@@ -635,7 +721,7 @@ class Module:
|
|
|
635
721
|
)
|
|
636
722
|
return int(channel)
|
|
637
723
|
|
|
638
|
-
def is_loaded(self) -> bool:
|
|
724
|
+
async def is_loaded(self) -> bool:
|
|
639
725
|
"""
|
|
640
726
|
Check if all name messages have been received
|
|
641
727
|
"""
|
|
@@ -653,7 +739,7 @@ class Module:
|
|
|
653
739
|
return False
|
|
654
740
|
# set that we finished the module loading
|
|
655
741
|
self.loaded = True
|
|
656
|
-
self._cache()
|
|
742
|
+
await self._cache()
|
|
657
743
|
return True
|
|
658
744
|
|
|
659
745
|
async def _request_module_status(self) -> None:
|
|
@@ -661,15 +747,20 @@ class Module:
|
|
|
661
747
|
if "Channels" not in self._data:
|
|
662
748
|
# some modules have no channels
|
|
663
749
|
return
|
|
750
|
+
self._log.info(f"Request module status {self._address}")
|
|
751
|
+
|
|
664
752
|
mod_stat_req_msg = ModuleStatusRequestMessage(self._address)
|
|
665
753
|
counter_msg = None
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if
|
|
671
|
-
|
|
672
|
-
|
|
754
|
+
if keys_exists(self._data, "AllChannelStatus"):
|
|
755
|
+
mod_stat_req_msg.channels = self._data["AllChannelStatus"]
|
|
756
|
+
else:
|
|
757
|
+
for chan, chan_data in self._data["Channels"].items():
|
|
758
|
+
if int(chan) < 9 and chan_data["Type"] in ("Blind", "Dimmer", "Relay"):
|
|
759
|
+
mod_stat_req_msg.channels.append(int(chan))
|
|
760
|
+
if chan_data["Type"] == "ButtonCounter":
|
|
761
|
+
if counter_msg is None:
|
|
762
|
+
counter_msg = CounterStatusRequestMessage(self._address)
|
|
763
|
+
counter_msg.channels.append(int(chan))
|
|
673
764
|
await self._writer(mod_stat_req_msg)
|
|
674
765
|
if counter_msg is not None:
|
|
675
766
|
await self._writer(counter_msg)
|
|
@@ -702,9 +793,10 @@ class Module:
|
|
|
702
793
|
self._name = None
|
|
703
794
|
return
|
|
704
795
|
|
|
705
|
-
for
|
|
706
|
-
|
|
707
|
-
|
|
796
|
+
for memory_key, memory_part in self._data["Memory"].items():
|
|
797
|
+
|
|
798
|
+
if memory_key == "Address":
|
|
799
|
+
for addr_int in memory_part.keys():
|
|
708
800
|
addr = struct.unpack(
|
|
709
801
|
">BB", struct.pack(">h", int("0x" + addr_int, 0))
|
|
710
802
|
)
|
|
@@ -714,34 +806,34 @@ class Module:
|
|
|
714
806
|
msg.low_address = addr[1]
|
|
715
807
|
await self._writer(msg)
|
|
716
808
|
|
|
717
|
-
async def
|
|
809
|
+
async def _load_default_channels(self) -> None:
|
|
718
810
|
if "Channels" not in self._data:
|
|
719
811
|
return
|
|
720
812
|
|
|
721
813
|
for chan, chan_data in self._data["Channels"].items():
|
|
722
814
|
edit = True
|
|
815
|
+
sub = True
|
|
723
816
|
if "Editable" not in chan_data or chan_data["Editable"] != "yes":
|
|
724
817
|
edit = False
|
|
818
|
+
if "Subdevice" not in chan_data or chan_data["Subdevice"] != "yes":
|
|
819
|
+
sub = False
|
|
725
820
|
cls = getattr(sys.modules[__name__], chan_data["Type"])
|
|
726
821
|
self._channels[int(chan)] = cls(
|
|
727
|
-
self,
|
|
822
|
+
module=self,
|
|
823
|
+
num=int(chan),
|
|
824
|
+
name=chan_data["Name"],
|
|
825
|
+
nameEditable=edit,
|
|
826
|
+
subDevice=sub,
|
|
827
|
+
writer=self._writer,
|
|
828
|
+
address=self._address,
|
|
728
829
|
)
|
|
729
830
|
if chan_data["Type"] == "Temperature":
|
|
730
831
|
if "Thermostat" in self._data or (
|
|
731
832
|
"ThermostatAddr" in self._data and self._data["ThermostatAddr"] != 0
|
|
732
833
|
):
|
|
733
834
|
await self._update_channel(int(chan), {"thermostat": True})
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if keys_exists(self._data, "Messages", "B3"):
|
|
737
|
-
self._channels[CHANNEL_SELECTED_PROGRAM] = SelectedProgram(
|
|
738
|
-
self,
|
|
739
|
-
CHANNEL_SELECTED_PROGRAM,
|
|
740
|
-
"Selected Program",
|
|
741
|
-
False,
|
|
742
|
-
self._writer,
|
|
743
|
-
self._address,
|
|
744
|
-
)
|
|
835
|
+
if chan_data["Type"] == "Dimmer" and "sliderScale" in self._data:
|
|
836
|
+
self._channels[int(chan)].slider_scale = self._data["sliderScale"]
|
|
745
837
|
|
|
746
838
|
|
|
747
839
|
class VmbDali(Module):
|
|
@@ -754,7 +846,6 @@ class VmbDali(Module):
|
|
|
754
846
|
self,
|
|
755
847
|
module_address: int,
|
|
756
848
|
module_type: int,
|
|
757
|
-
module_data: dict,
|
|
758
849
|
serial: int | None = None,
|
|
759
850
|
memorymap: int | None = None,
|
|
760
851
|
build_year: int | None = None,
|
|
@@ -764,7 +855,6 @@ class VmbDali(Module):
|
|
|
764
855
|
super().__init__(
|
|
765
856
|
module_address,
|
|
766
857
|
module_type,
|
|
767
|
-
module_data,
|
|
768
858
|
serial,
|
|
769
859
|
memorymap,
|
|
770
860
|
build_year,
|
|
@@ -773,19 +863,24 @@ class VmbDali(Module):
|
|
|
773
863
|
)
|
|
774
864
|
self.group_members: dict[int, set[int]] = {}
|
|
775
865
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
866
|
+
def get_initial_timeout(self) -> int:
|
|
867
|
+
return 100000
|
|
868
|
+
|
|
869
|
+
async def _load_default_channels(self) -> None:
|
|
870
|
+
for chan in range(1, 64 + 1):
|
|
871
|
+
self._channels[chan] = Channel(
|
|
872
|
+
module=self,
|
|
873
|
+
num=chan,
|
|
874
|
+
name="placeholder",
|
|
875
|
+
nameEditable=True,
|
|
876
|
+
subDevice=True,
|
|
877
|
+
writer=self._writer,
|
|
878
|
+
address=self._address,
|
|
879
|
+
)
|
|
880
|
+
# Placeholders will keep this module loading
|
|
881
|
+
# Until the DaliDeviceSettings messages either delete or replace these placeholder's
|
|
882
|
+
# with actual channels
|
|
883
|
+
await self._request_dali_channels()
|
|
789
884
|
|
|
790
885
|
async def _request_dali_channels(self):
|
|
791
886
|
msg_type = commandRegistry.get_command(
|
|
@@ -804,13 +899,32 @@ class VmbDali(Module):
|
|
|
804
899
|
if message.channel in self._channels:
|
|
805
900
|
del self._channels[message.channel]
|
|
806
901
|
elif message.data.device_type == DaliDeviceType.LedModule:
|
|
807
|
-
|
|
902
|
+
cache = self._loaded_cache
|
|
903
|
+
if (
|
|
904
|
+
"channels" in cache
|
|
905
|
+
and str(message.channel) in cache["channels"]
|
|
906
|
+
and cache["channels"][str(message.channel)]["type"] == "Dimmer"
|
|
907
|
+
):
|
|
908
|
+
# If we have a cached dimmer channel, use that name
|
|
909
|
+
name = cache["channels"][str(message.channel)]["name"]
|
|
910
|
+
self._channels[message.channel] = Dimmer(
|
|
911
|
+
self,
|
|
912
|
+
message.channel,
|
|
913
|
+
name,
|
|
914
|
+
False, # set False to enable an already loaded Dimmer
|
|
915
|
+
True,
|
|
916
|
+
self._writer,
|
|
917
|
+
self._address,
|
|
918
|
+
slider_scale=254,
|
|
919
|
+
)
|
|
920
|
+
elif self._channels.get(message.channel).__class__ != Dimmer:
|
|
808
921
|
# New or changed type, replace channel:
|
|
809
922
|
self._channels[message.channel] = Dimmer(
|
|
810
923
|
self,
|
|
811
924
|
message.channel,
|
|
812
925
|
None,
|
|
813
926
|
True,
|
|
927
|
+
True,
|
|
814
928
|
self._writer,
|
|
815
929
|
self._address,
|
|
816
930
|
slider_scale=254,
|
|
@@ -860,8 +974,6 @@ class VmbDali(Module):
|
|
|
860
974
|
else:
|
|
861
975
|
return await super().on_message(message)
|
|
862
976
|
|
|
863
|
-
self._cache()
|
|
864
|
-
|
|
865
977
|
async def _request_channel_name(self) -> None:
|
|
866
978
|
# Channel names are requested after channel scan
|
|
867
979
|
# don't do them here (at initialization time)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Channels": {
|
|
3
|
+
"01": {
|
|
4
|
+
"Editable": "yes",
|
|
5
|
+
"Name": "Push button 1",
|
|
6
|
+
"Type": "Button"
|
|
7
|
+
},
|
|
8
|
+
"02": {
|
|
9
|
+
"Editable": "yes",
|
|
10
|
+
"Name": "Push button 2",
|
|
11
|
+
"Type": "Button"
|
|
12
|
+
},
|
|
13
|
+
"03": {
|
|
14
|
+
"Editable": "yes",
|
|
15
|
+
"Name": "Push button 3",
|
|
16
|
+
"Type": "Button"
|
|
17
|
+
},
|
|
18
|
+
"04": {
|
|
19
|
+
"Editable": "yes",
|
|
20
|
+
"Name": "Push button 4",
|
|
21
|
+
"Type": "Button"
|
|
22
|
+
},
|
|
23
|
+
"05": {
|
|
24
|
+
"Editable": "yes",
|
|
25
|
+
"Name": "Push button 5",
|
|
26
|
+
"Type": "Button"
|
|
27
|
+
},
|
|
28
|
+
"06": {
|
|
29
|
+
"Editable": "yes",
|
|
30
|
+
"Name": "Push button 6",
|
|
31
|
+
"Type": "Button"
|
|
32
|
+
},
|
|
33
|
+
"07": {
|
|
34
|
+
"Editable": "yes",
|
|
35
|
+
"Name": "Push button 7",
|
|
36
|
+
"Type": "Button"
|
|
37
|
+
},
|
|
38
|
+
"08": {
|
|
39
|
+
"Editable": "yes",
|
|
40
|
+
"Name": "Push button 8",
|
|
41
|
+
"Type": "Button"
|
|
42
|
+
},
|
|
43
|
+
"96": {
|
|
44
|
+
"Name": "SelectedProgram",
|
|
45
|
+
"Type": "SelectedProgram"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"Info": "8-Channel Push Button Module",
|
|
49
|
+
"Type": "VMB8PB",
|
|
50
|
+
"Memory": {
|
|
51
|
+
"Channels": {
|
|
52
|
+
"01": "0000-000E",
|
|
53
|
+
"02": "0010-001E",
|
|
54
|
+
"03": "0020-002E",
|
|
55
|
+
"04": "0030-003E",
|
|
56
|
+
"05": "0040-004E",
|
|
57
|
+
"06": "0050-005E",
|
|
58
|
+
"07": "0060-006E",
|
|
59
|
+
"08": "0070-007E"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|