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/module.py
CHANGED
|
@@ -1,30 +1,51 @@
|
|
|
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
|
-
import
|
|
12
|
+
import pathlib
|
|
9
13
|
import struct
|
|
10
14
|
import sys
|
|
15
|
+
from typing import Awaitable, Callable
|
|
16
|
+
|
|
17
|
+
from aiofile import async_open
|
|
11
18
|
|
|
12
19
|
from velbusaio.channels import (
|
|
13
20
|
Blind,
|
|
14
21
|
Button,
|
|
15
22
|
ButtonCounter,
|
|
23
|
+
Channel,
|
|
16
24
|
Dimmer,
|
|
17
25
|
EdgeLit,
|
|
18
26
|
LightSensor,
|
|
19
27
|
Memo,
|
|
20
28
|
Relay,
|
|
29
|
+
SelectedProgram,
|
|
21
30
|
Sensor,
|
|
22
31
|
SensorNumber,
|
|
23
|
-
|
|
32
|
+
)
|
|
33
|
+
from velbusaio.channels import Temperature
|
|
34
|
+
from velbusaio.channels import Temperature as TemperatureChannelType
|
|
35
|
+
from velbusaio.channels import (
|
|
24
36
|
ThermostatChannel,
|
|
25
37
|
)
|
|
26
|
-
from velbusaio.
|
|
27
|
-
from velbusaio.
|
|
38
|
+
from velbusaio.command_registry import commandRegistry
|
|
39
|
+
from velbusaio.const import (
|
|
40
|
+
CHANNEL_LIGHT_VALUE,
|
|
41
|
+
CHANNEL_MEMO_TEXT,
|
|
42
|
+
CHANNEL_SELECTED_PROGRAM,
|
|
43
|
+
PRIORITY_LOW,
|
|
44
|
+
SCAN_MODULEINFO_TIMEOUT_INITIAL,
|
|
45
|
+
)
|
|
46
|
+
from velbusaio.helpers import h2, handle_match, keys_exists
|
|
47
|
+
from velbusaio.message import Message
|
|
48
|
+
from velbusaio.messages.blind_status import BlindStatusMessage, BlindStatusNgMessage
|
|
28
49
|
from velbusaio.messages.channel_name_part1 import (
|
|
29
50
|
ChannelNamePart1Message,
|
|
30
51
|
ChannelNamePart1Message2,
|
|
@@ -40,24 +61,42 @@ from velbusaio.messages.channel_name_part3 import (
|
|
|
40
61
|
ChannelNamePart3Message2,
|
|
41
62
|
ChannelNamePart3Message3,
|
|
42
63
|
)
|
|
43
|
-
from velbusaio.messages.channel_name_request import
|
|
64
|
+
from velbusaio.messages.channel_name_request import (
|
|
65
|
+
COMMAND_CODE as CHANNEL_NAME_REQUEST_COMMAND_CODE,
|
|
66
|
+
)
|
|
67
|
+
from velbusaio.messages.channel_name_request import (
|
|
68
|
+
ChannelNameRequestMessage,
|
|
69
|
+
)
|
|
44
70
|
from velbusaio.messages.clear_led import ClearLedMessage
|
|
45
71
|
from velbusaio.messages.counter_status import CounterStatusMessage
|
|
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
|
|
75
|
+
from velbusaio.messages.dali_device_settings import DeviceType as DaliDeviceType
|
|
76
|
+
from velbusaio.messages.dali_device_settings import DeviceTypeMsg as DaliDeviceTypeMsg
|
|
77
|
+
from velbusaio.messages.dali_device_settings import MemberOfGroupMsg
|
|
78
|
+
from velbusaio.messages.dali_device_settings_request import (
|
|
79
|
+
COMMAND_CODE as DALI_DEVICE_SETTINGS_REQUEST_COMMAND_CODE,
|
|
80
|
+
)
|
|
81
|
+
from velbusaio.messages.dali_device_settings_request import (
|
|
82
|
+
DaliDeviceSettingsRequest,
|
|
83
|
+
)
|
|
84
|
+
from velbusaio.messages.dali_dim_value_status import DimValueStatus
|
|
46
85
|
from velbusaio.messages.dimmer_channel_status import DimmerChannelStatusMessage
|
|
47
86
|
from velbusaio.messages.dimmer_status import DimmerStatusMessage
|
|
48
87
|
from velbusaio.messages.fast_blinking_led import FastBlinkingLedMessage
|
|
49
88
|
from velbusaio.messages.memory_data import MemoryDataMessage
|
|
50
89
|
from velbusaio.messages.module_status import (
|
|
90
|
+
ModuleStatusGP4PirMessage,
|
|
51
91
|
ModuleStatusMessage,
|
|
52
92
|
ModuleStatusMessage2,
|
|
53
93
|
ModuleStatusPirMessage,
|
|
54
94
|
)
|
|
55
95
|
from velbusaio.messages.module_status_request import ModuleStatusRequestMessage
|
|
56
|
-
from velbusaio.messages.module_subtype import ModuleSubTypeMessage
|
|
57
|
-
from velbusaio.messages.module_type import ModuleTypeMessage
|
|
58
96
|
from velbusaio.messages.push_button_status import PushButtonStatusMessage
|
|
97
|
+
from velbusaio.messages.raw import MeteoRawMessage, SensorRawMessage
|
|
59
98
|
from velbusaio.messages.read_data_from_memory import ReadDataFromMemoryMessage
|
|
60
|
-
from velbusaio.messages.relay_status import RelayStatusMessage
|
|
99
|
+
from velbusaio.messages.relay_status import RelayStatusMessage, RelayStatusMessage2
|
|
61
100
|
from velbusaio.messages.sensor_temperature import SensorTemperatureMessage
|
|
62
101
|
from velbusaio.messages.set_led import SetLedMessage
|
|
63
102
|
from velbusaio.messages.slider_status import SliderStatusMessage
|
|
@@ -71,19 +110,51 @@ class Module:
|
|
|
71
110
|
Abstract class for Velbus hardware modules.
|
|
72
111
|
"""
|
|
73
112
|
|
|
113
|
+
@classmethod
|
|
114
|
+
def factory(
|
|
115
|
+
cls,
|
|
116
|
+
module_address: int,
|
|
117
|
+
module_type: int,
|
|
118
|
+
serial: int | None = None,
|
|
119
|
+
memorymap: int | None = None,
|
|
120
|
+
build_year: int | None = None,
|
|
121
|
+
build_week: int | None = None,
|
|
122
|
+
cache_dir: str | None = None,
|
|
123
|
+
) -> Module:
|
|
124
|
+
if module_type == 0x45 or module_type == 0x5A:
|
|
125
|
+
return VmbDali(
|
|
126
|
+
module_address,
|
|
127
|
+
module_type,
|
|
128
|
+
serial,
|
|
129
|
+
memorymap,
|
|
130
|
+
build_year,
|
|
131
|
+
build_week,
|
|
132
|
+
cache_dir,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return Module(
|
|
136
|
+
module_address,
|
|
137
|
+
module_type,
|
|
138
|
+
serial,
|
|
139
|
+
memorymap,
|
|
140
|
+
build_year,
|
|
141
|
+
build_week,
|
|
142
|
+
cache_dir,
|
|
143
|
+
)
|
|
144
|
+
|
|
74
145
|
def __init__(
|
|
75
146
|
self,
|
|
76
147
|
module_address: int,
|
|
77
148
|
module_type: int,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
149
|
+
serial: int | None = None,
|
|
150
|
+
memorymap: int | None = None,
|
|
151
|
+
build_year: int | None = None,
|
|
152
|
+
build_week: int | None = None,
|
|
153
|
+
cache_dir: str | None = None,
|
|
83
154
|
) -> None:
|
|
84
155
|
self._address = module_address
|
|
85
|
-
self._type = module_type
|
|
86
|
-
self._data =
|
|
156
|
+
self._type = int(module_type)
|
|
157
|
+
self._data = {}
|
|
87
158
|
|
|
88
159
|
self._name = {}
|
|
89
160
|
self._sub_address = {}
|
|
@@ -91,31 +162,79 @@ class Module:
|
|
|
91
162
|
self.memory_map_version = memorymap
|
|
92
163
|
self.build_year = build_year
|
|
93
164
|
self.build_week = build_week
|
|
165
|
+
self._cache_dir = cache_dir
|
|
94
166
|
self._is_loading = False
|
|
167
|
+
self._got_status = asyncio.Event()
|
|
168
|
+
self._got_status.clear()
|
|
95
169
|
self._channels = {}
|
|
96
170
|
self.loaded = False
|
|
171
|
+
self._use_cache = True
|
|
172
|
+
self._loaded_cache = {}
|
|
97
173
|
|
|
98
|
-
def
|
|
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}")
|
|
179
|
+
|
|
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:
|
|
99
184
|
self._log = logging.getLogger("velbus-module")
|
|
100
|
-
|
|
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
|
|
101
208
|
self._writer = writer
|
|
102
209
|
for chan in self._channels.values():
|
|
103
210
|
chan._writer = writer
|
|
104
211
|
|
|
105
212
|
def cleanupSubChannels(self) -> None:
|
|
106
|
-
|
|
107
|
-
|
|
213
|
+
# TODO: 21/11/2022 DannyDeGaspari: Fix needed
|
|
214
|
+
# Care should be taken for this function, not all subaddresses have their channels on multiples of 8.
|
|
215
|
+
# The last subaddress contain typically the temperature channels, has more then 8 channels
|
|
216
|
+
# and doesn't start on a boundary of 8.
|
|
217
|
+
# E.g. The VMBGP4 has one subaddress, so since the second subaddress is not defined,
|
|
218
|
+
# this function will delete channels 17-24 while 17 and 18 belong to the temperature channels.
|
|
219
|
+
#
|
|
220
|
+
# The solution would be that this functions knows were the temperature channels are located
|
|
221
|
+
# and/or what the max number of subaddresses are for each module.
|
|
222
|
+
# if self._sub_address == {} and self.loaded:
|
|
223
|
+
# raise Exception("No subaddresses defined")
|
|
108
224
|
for sub in range(1, 4):
|
|
109
225
|
if sub not in self._sub_address:
|
|
110
226
|
for i in range(((sub * 8) + 1), (((sub + 1) * 8) + 1)):
|
|
111
|
-
if i in self._channels
|
|
227
|
+
if i in self._channels and not isinstance(
|
|
228
|
+
self._channels[i], TemperatureChannelType
|
|
229
|
+
):
|
|
112
230
|
del self._channels[i]
|
|
113
231
|
|
|
114
|
-
def _cache(self) -> None:
|
|
115
|
-
if not
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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))
|
|
119
238
|
|
|
120
239
|
def __getstate__(self) -> dict:
|
|
121
240
|
d = self.__dict__
|
|
@@ -126,20 +245,20 @@ class Module:
|
|
|
126
245
|
self.__dict__ = state
|
|
127
246
|
|
|
128
247
|
def __repr__(self) -> str:
|
|
129
|
-
return
|
|
130
|
-
"<{}: {{{}}} @ {{{}}} loaded:{{{}}} loading:{{{}}} channels{{:{}}}>".format(
|
|
131
|
-
self._name,
|
|
132
|
-
self._type,
|
|
133
|
-
self._address,
|
|
134
|
-
self.loaded,
|
|
135
|
-
self._is_loading,
|
|
136
|
-
self._channels,
|
|
137
|
-
)
|
|
138
|
-
)
|
|
248
|
+
return f"<{self._name} type:{self._type} address:{self._address} loaded:{self.loaded} loading:{self._is_loading} channels: {self._channels}>"
|
|
139
249
|
|
|
140
250
|
def __str__(self) -> str:
|
|
141
251
|
return self.__repr__()
|
|
142
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
|
+
|
|
143
262
|
def get_addresses(self) -> list:
|
|
144
263
|
"""
|
|
145
264
|
Get all addresses for this module
|
|
@@ -157,26 +276,35 @@ class Module:
|
|
|
157
276
|
return self._type
|
|
158
277
|
|
|
159
278
|
def get_type_name(self) -> str:
|
|
160
|
-
|
|
279
|
+
if "Type" in self._data:
|
|
280
|
+
return self._data["Type"]
|
|
281
|
+
return "UNKNOWN"
|
|
161
282
|
|
|
162
|
-
def get_serial(self) -> str:
|
|
283
|
+
def get_serial(self) -> str | None:
|
|
163
284
|
return self.serial
|
|
164
285
|
|
|
165
286
|
def get_name(self) -> str:
|
|
166
287
|
return self._name
|
|
167
288
|
|
|
168
289
|
def get_sw_version(self) -> str:
|
|
169
|
-
return "{}-{}.{}.{}"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
290
|
+
return f"{self.serial}-{self.memory_map_version}.{self.build_year}.{self.build_week}"
|
|
291
|
+
|
|
292
|
+
def calc_channel_offset(self, address: int) -> int:
|
|
293
|
+
_channel_offset = 0
|
|
294
|
+
if self._address != address:
|
|
295
|
+
for _sub_addr_key, _sub_addr_val in self._sub_address.items():
|
|
296
|
+
if _sub_addr_val == address:
|
|
297
|
+
_channel_offset = 8 * _sub_addr_key
|
|
298
|
+
break
|
|
299
|
+
return _channel_offset
|
|
300
|
+
|
|
301
|
+
async def on_message(self, message: Message) -> None:
|
|
177
302
|
"""
|
|
178
303
|
Process received message
|
|
179
304
|
"""
|
|
305
|
+
self._log.debug(f"RX: {message}")
|
|
306
|
+
_channel_offset = self.calc_channel_offset(message.address)
|
|
307
|
+
|
|
180
308
|
if isinstance(
|
|
181
309
|
message,
|
|
182
310
|
(
|
|
@@ -186,6 +314,7 @@ class Module:
|
|
|
186
314
|
),
|
|
187
315
|
):
|
|
188
316
|
self._process_channel_name_message(1, message)
|
|
317
|
+
await self._cache()
|
|
189
318
|
elif isinstance(
|
|
190
319
|
message,
|
|
191
320
|
(
|
|
@@ -195,6 +324,7 @@ class Module:
|
|
|
195
324
|
),
|
|
196
325
|
):
|
|
197
326
|
self._process_channel_name_message(2, message)
|
|
327
|
+
await self._cache()
|
|
198
328
|
elif isinstance(
|
|
199
329
|
message,
|
|
200
330
|
(
|
|
@@ -204,135 +334,314 @@ class Module:
|
|
|
204
334
|
),
|
|
205
335
|
):
|
|
206
336
|
self._process_channel_name_message(3, message)
|
|
337
|
+
await self._cache()
|
|
207
338
|
elif isinstance(message, MemoryDataMessage):
|
|
208
339
|
await self._process_memory_data_message(message)
|
|
209
|
-
elif isinstance(message, RelayStatusMessage):
|
|
210
|
-
await self.
|
|
340
|
+
elif isinstance(message, (RelayStatusMessage, RelayStatusMessage2)):
|
|
341
|
+
await self._update_channel(
|
|
342
|
+
message.channel,
|
|
343
|
+
{
|
|
344
|
+
"on": message.is_on(),
|
|
345
|
+
"inhibit": message.is_inhibited(),
|
|
346
|
+
"forced_on": message.is_forced_on(),
|
|
347
|
+
"disabled": message.is_disabled(),
|
|
348
|
+
},
|
|
349
|
+
)
|
|
211
350
|
elif isinstance(message, SensorTemperatureMessage):
|
|
212
351
|
chan = self._translate_channel_name(self._data["TemperatureChannel"])
|
|
213
|
-
await self._channels[chan].
|
|
352
|
+
await self._channels[chan].maybe_update_temperature(
|
|
353
|
+
message.getCurTemp(), 1 / 64
|
|
354
|
+
)
|
|
355
|
+
await self._update_channel(
|
|
356
|
+
chan,
|
|
214
357
|
{
|
|
215
|
-
"cur": message.getCurTemp(),
|
|
216
358
|
"min": message.getMinTemp(),
|
|
217
359
|
"max": message.getMaxTemp(),
|
|
218
|
-
}
|
|
360
|
+
},
|
|
219
361
|
)
|
|
220
362
|
elif isinstance(message, TempSensorStatusMessage):
|
|
221
363
|
# update the current temp
|
|
222
364
|
chan = self._translate_channel_name(self._data["TemperatureChannel"])
|
|
223
365
|
if chan in self._channels:
|
|
224
|
-
await self.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
366
|
+
await self._update_channel(
|
|
367
|
+
chan,
|
|
368
|
+
{
|
|
369
|
+
"target": message.target_temp,
|
|
370
|
+
"cmode": message.mode_str,
|
|
371
|
+
"cstatus": message.status_str,
|
|
372
|
+
"sleep_timer": message.sleep_timer,
|
|
373
|
+
"cool_mode": message.cool_mode,
|
|
374
|
+
},
|
|
375
|
+
)
|
|
376
|
+
await self._channels[chan].maybe_update_temperature(
|
|
377
|
+
message.current_temp, 1 / 2
|
|
378
|
+
)
|
|
379
|
+
# update the thermostat channels
|
|
380
|
+
channel_name_to_msg_prop_map = {
|
|
381
|
+
"Heater": "heater",
|
|
382
|
+
"Boost": "boost",
|
|
383
|
+
"Pump": "pump",
|
|
384
|
+
"Cooler": "cooler",
|
|
385
|
+
"Alarm 1": "alarm1",
|
|
386
|
+
"Alarm 2": "alarm2",
|
|
387
|
+
"Alarm 3": "alarm3",
|
|
388
|
+
"Alarm 4": "alarm4",
|
|
389
|
+
}
|
|
390
|
+
for channel_str in self._data["Channels"]:
|
|
391
|
+
if keys_exists(self._data, "Channels", channel_str, "Type"):
|
|
392
|
+
if (
|
|
393
|
+
self._data["Channels"][channel_str]["Type"]
|
|
394
|
+
== "ThermostatChannel"
|
|
395
|
+
):
|
|
396
|
+
channel = self._translate_channel_name(channel_str)
|
|
397
|
+
channel_name = self._data["Channels"][channel_str]["Name"]
|
|
398
|
+
if channel in self._channels:
|
|
399
|
+
await self._update_channel(
|
|
400
|
+
channel,
|
|
401
|
+
{
|
|
402
|
+
"closed": getattr(
|
|
403
|
+
message,
|
|
404
|
+
channel_name_to_msg_prop_map[channel_name],
|
|
405
|
+
)
|
|
406
|
+
},
|
|
407
|
+
)
|
|
228
408
|
elif isinstance(message, PushButtonStatusMessage):
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
409
|
+
_update_buttons = False
|
|
410
|
+
for channel_types in self._data["Channels"]:
|
|
411
|
+
if keys_exists(self._data, "Channels", channel_types, "Type"):
|
|
412
|
+
if (
|
|
413
|
+
self._data["Channels"][channel_types]["Type"] == "Button"
|
|
414
|
+
or self._data["Channels"][channel_types]["Type"] == "Sensor"
|
|
415
|
+
or self._data["Channels"][channel_types]["Type"]
|
|
416
|
+
== "ButtonCounter"
|
|
417
|
+
):
|
|
418
|
+
_update_buttons = True
|
|
419
|
+
break
|
|
420
|
+
if _update_buttons:
|
|
421
|
+
for channel_id in range(1, 9):
|
|
422
|
+
channel = self._translate_channel_name(channel_id + _channel_offset)
|
|
423
|
+
if channel_id in message.closed:
|
|
424
|
+
await self._update_channel(channel, {"closed": True})
|
|
425
|
+
if channel_id in message.closed_long:
|
|
426
|
+
await self._update_channel(channel, {"long": True})
|
|
427
|
+
if channel_id in message.opened:
|
|
428
|
+
await self._update_channel(
|
|
429
|
+
channel, {"closed": False, "long": False}
|
|
430
|
+
)
|
|
431
|
+
elif isinstance(message, (ModuleStatusMessage)):
|
|
432
|
+
for channel_id in range(1, 9):
|
|
433
|
+
channel = self._translate_channel_name(channel_id + _channel_offset)
|
|
434
|
+
if channel_id in message.closed:
|
|
435
|
+
await self._update_channel(channel, {"closed": True})
|
|
436
|
+
elif channel in self._channels and isinstance(
|
|
437
|
+
self._channels[channel], (Button, ButtonCounter)
|
|
438
|
+
):
|
|
439
|
+
await self._update_channel(channel, {"closed": False})
|
|
440
|
+
elif isinstance(message, (ModuleStatusMessage2)):
|
|
441
|
+
for channel_id in range(1, 9):
|
|
442
|
+
channel = self._translate_channel_name(channel_id + _channel_offset)
|
|
443
|
+
if channel_id in message.closed:
|
|
444
|
+
await self._update_channel(channel, {"closed": True})
|
|
247
445
|
elif isinstance(self._channels[channel], (Button, ButtonCounter)):
|
|
248
|
-
await self.
|
|
446
|
+
await self._update_channel(channel, {"closed": False})
|
|
447
|
+
if channel_id in message.enabled:
|
|
448
|
+
await self._update_channel(channel, {"enabled": True})
|
|
449
|
+
elif channel in self._channels and isinstance(
|
|
450
|
+
self._channels[channel], (Button, ButtonCounter)
|
|
451
|
+
):
|
|
452
|
+
await self._update_channel(channel, {"enabled": False})
|
|
453
|
+
# self.selected_program_str = message.selected_program_str
|
|
454
|
+
await self._update_channel(
|
|
455
|
+
CHANNEL_SELECTED_PROGRAM,
|
|
456
|
+
{"selected_program_str": message.selected_program_str},
|
|
457
|
+
)
|
|
249
458
|
elif isinstance(message, CounterStatusMessage) and isinstance(
|
|
250
459
|
self._channels[message.channel], ButtonCounter
|
|
251
460
|
):
|
|
252
|
-
|
|
461
|
+
channel = self._translate_channel_name(message.channel)
|
|
462
|
+
await self._update_channel(
|
|
463
|
+
channel,
|
|
253
464
|
{
|
|
254
465
|
"pulses": message.pulses,
|
|
255
466
|
"counter": message.counter,
|
|
256
467
|
"delay": message.delay,
|
|
257
|
-
}
|
|
468
|
+
},
|
|
258
469
|
)
|
|
259
470
|
elif isinstance(message, ModuleStatusPirMessage):
|
|
260
|
-
await self.
|
|
471
|
+
await self._update_channel(
|
|
472
|
+
CHANNEL_LIGHT_VALUE, {"cur": message.light_value}
|
|
473
|
+
)
|
|
474
|
+
await self._update_channel(1, {"closed": message.dark})
|
|
475
|
+
await self._update_channel(2, {"closed": message.light})
|
|
476
|
+
await self._update_channel(3, {"closed": message.motion1})
|
|
477
|
+
await self._update_channel(4, {"closed": message.light_motion1})
|
|
478
|
+
await self._update_channel(5, {"closed": message.motion2})
|
|
479
|
+
await self._update_channel(6, {"closed": message.light_motion2})
|
|
480
|
+
if 7 in self._channels:
|
|
481
|
+
await self._update_channel(7, {"closed": message.low_temp_alarm})
|
|
482
|
+
if 8 in self._channels:
|
|
483
|
+
await self._update_channel(8, {"closed": message.high_temp_alarm})
|
|
484
|
+
# self.selected_program_str = message.selected_program_str
|
|
485
|
+
await self._update_channel(
|
|
486
|
+
CHANNEL_SELECTED_PROGRAM,
|
|
487
|
+
{"selected_program_str": message.selected_program_str},
|
|
488
|
+
)
|
|
489
|
+
elif isinstance(message, ModuleStatusGP4PirMessage):
|
|
490
|
+
await self._update_channel(
|
|
491
|
+
CHANNEL_LIGHT_VALUE, {"cur": message.light_value}
|
|
492
|
+
)
|
|
493
|
+
for channel_id in range(1, 9):
|
|
494
|
+
channel = self._translate_channel_name(channel_id + _channel_offset)
|
|
495
|
+
await self._update_channel(
|
|
496
|
+
channel, {"closed": channel_id in message.closed}
|
|
497
|
+
)
|
|
498
|
+
if type(self._channels[channel]) is Button:
|
|
499
|
+
# only treat 'enabled' if the channel is a Button
|
|
500
|
+
await self._update_channel(
|
|
501
|
+
channel, {"enabled": channel_id in message.enabled}
|
|
502
|
+
)
|
|
503
|
+
# self.selected_program_str = message.selected_program_str
|
|
504
|
+
await self._update_channel(
|
|
505
|
+
CHANNEL_SELECTED_PROGRAM,
|
|
506
|
+
{"selected_program_str": message.selected_program_str},
|
|
507
|
+
)
|
|
261
508
|
elif isinstance(message, UpdateLedStatusMessage):
|
|
262
|
-
for
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
509
|
+
for channel_id in range(1, 9):
|
|
510
|
+
channel = self._translate_channel_name(channel_id + _channel_offset)
|
|
511
|
+
if channel_id in message.led_slow_blinking:
|
|
512
|
+
await self._update_channel(channel, {"led_state": "slow"})
|
|
513
|
+
if channel_id in message.led_fast_blinking:
|
|
514
|
+
await self._update_channel(channel, {"led_state": "fast"})
|
|
515
|
+
if channel_id in message.led_on:
|
|
516
|
+
await self._update_channel(channel, {"led_state": "on"})
|
|
269
517
|
if (
|
|
270
|
-
|
|
271
|
-
and
|
|
272
|
-
and
|
|
518
|
+
channel_id not in message.led_slow_blinking
|
|
519
|
+
and channel_id not in message.led_fast_blinking
|
|
520
|
+
and channel_id not in message.led_on
|
|
273
521
|
):
|
|
274
|
-
await self.
|
|
522
|
+
await self._update_channel(channel, {"led_state": "off"})
|
|
275
523
|
elif isinstance(message, SetLedMessage):
|
|
276
|
-
for
|
|
277
|
-
|
|
278
|
-
|
|
524
|
+
for channel_id in range(1, 9):
|
|
525
|
+
channel = self._translate_channel_name(channel_id + _channel_offset)
|
|
526
|
+
if channel_id in message.leds:
|
|
527
|
+
await self._update_channel(channel, {"led_state": "on"})
|
|
279
528
|
elif isinstance(message, ClearLedMessage):
|
|
280
|
-
for
|
|
281
|
-
|
|
282
|
-
|
|
529
|
+
for channel_id in range(1, 9):
|
|
530
|
+
channel = self._translate_channel_name(channel_id + _channel_offset)
|
|
531
|
+
if channel_id in message.leds:
|
|
532
|
+
await self._update_channel(channel, {"led_state": "off"})
|
|
283
533
|
elif isinstance(message, SlowBlinkingLedMessage):
|
|
284
|
-
for
|
|
285
|
-
|
|
286
|
-
|
|
534
|
+
for channel_id in range(1, 9):
|
|
535
|
+
channel = self._translate_channel_name(channel_id + _channel_offset)
|
|
536
|
+
if channel_id in message.leds:
|
|
537
|
+
await self._update_channel(channel, {"led_state": "slow"})
|
|
287
538
|
elif isinstance(message, FastBlinkingLedMessage):
|
|
288
|
-
for
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)
|
|
539
|
+
for channel_id in range(1, 9):
|
|
540
|
+
channel = self._translate_channel_name(channel_id + _channel_offset)
|
|
541
|
+
if channel_id in message.leds:
|
|
542
|
+
await self._update_channel(channel, {"led_state": "fast"})
|
|
543
|
+
elif isinstance(message, (DimmerChannelStatusMessage, DimmerStatusMessage)):
|
|
544
|
+
channel = self._translate_channel_name(message.channel)
|
|
545
|
+
await self._update_channel(channel, {"state": message.cur_dimmer_state()})
|
|
295
546
|
elif isinstance(message, SliderStatusMessage):
|
|
296
|
-
|
|
297
|
-
|
|
547
|
+
channel = self._translate_channel_name(message.channel)
|
|
548
|
+
await self._update_channel(channel, {"state": message.cur_slider_state()})
|
|
549
|
+
elif isinstance(message, BlindStatusNgMessage):
|
|
550
|
+
channel = self._translate_channel_name(message.channel)
|
|
551
|
+
await self._update_channel(
|
|
552
|
+
channel, {"state": message.status, "position": message.position}
|
|
553
|
+
)
|
|
554
|
+
elif isinstance(message, BlindStatusMessage):
|
|
555
|
+
channel = self._translate_channel_name(message.channel)
|
|
556
|
+
await self._update_channel(channel, {"state": message.status})
|
|
557
|
+
elif isinstance(message, MeteoRawMessage):
|
|
558
|
+
await self._update_channel(11, {"cur": message.rain})
|
|
559
|
+
await self._update_channel(12, {"cur": message.light})
|
|
560
|
+
await self._update_channel(13, {"cur": message.wind})
|
|
561
|
+
elif isinstance(message, SensorRawMessage):
|
|
562
|
+
await self._update_channel(
|
|
563
|
+
message.sensor, {"cur": message.value, "unit": message.unit}
|
|
298
564
|
)
|
|
299
|
-
elif isinstance(message,
|
|
300
|
-
await self.
|
|
301
|
-
{"
|
|
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()
|
|
575
|
+
|
|
576
|
+
async def _update_channel(self, channel: int, updates: dict):
|
|
577
|
+
try:
|
|
578
|
+
await self._channels[channel].update(updates)
|
|
579
|
+
except KeyError:
|
|
580
|
+
self._log.info(
|
|
581
|
+
f"channel {channel} does not exist for module @ address {self}"
|
|
302
582
|
)
|
|
303
|
-
self._cache()
|
|
304
583
|
|
|
305
|
-
def get_channels(self) ->
|
|
584
|
+
def get_channels(self) -> dict:
|
|
306
585
|
"""
|
|
307
586
|
List all channels for this module
|
|
308
587
|
"""
|
|
309
588
|
return self._channels
|
|
310
589
|
|
|
311
|
-
async def
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
self.
|
|
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
|
+
|
|
602
|
+
async def load(self, from_cache: bool = False) -> None:
|
|
322
603
|
# start the loading
|
|
323
604
|
self._is_loading = True
|
|
605
|
+
# see if we have a cache
|
|
606
|
+
cache = await self._get_cache()
|
|
607
|
+
self._loaded_cache = cache
|
|
324
608
|
# load default channels
|
|
325
|
-
self.
|
|
609
|
+
await self._load_default_channels()
|
|
610
|
+
|
|
326
611
|
# load the data from memory ( the stuff that we need)
|
|
327
|
-
|
|
612
|
+
if "name" in cache and cache["name"] != "":
|
|
613
|
+
self._name = cache["name"]
|
|
614
|
+
else:
|
|
615
|
+
await self.__load_memory()
|
|
328
616
|
# load the module status
|
|
329
|
-
await self._request_module_status()
|
|
617
|
+
# await self._request_module_status()
|
|
330
618
|
# load the channel names
|
|
331
|
-
|
|
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()
|
|
332
631
|
# load the module specific stuff
|
|
333
632
|
self._load()
|
|
334
633
|
# stop the loading
|
|
335
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
|
|
336
645
|
|
|
337
646
|
def _load(self) -> None:
|
|
338
647
|
"""
|
|
@@ -350,31 +659,50 @@ class Module:
|
|
|
350
659
|
return 0
|
|
351
660
|
return max(self._channels.keys())
|
|
352
661
|
|
|
353
|
-
async def
|
|
662
|
+
async def set_memo_text(self, txt: str) -> None:
|
|
663
|
+
if CHANNEL_MEMO_TEXT not in self._channels.keys():
|
|
664
|
+
return
|
|
665
|
+
await self._channels[CHANNEL_MEMO_TEXT].set(txt)
|
|
666
|
+
|
|
667
|
+
async def _process_memory_data_message(self, message: MemoryDataMessage) -> None:
|
|
354
668
|
addr = "{high:02X}{low:02X}".format(
|
|
355
669
|
high=message.high_address, low=message.low_address
|
|
356
670
|
)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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]
|
|
676
|
+
if "ModuleName" in mdata and isinstance(self._name, dict):
|
|
677
|
+
# if self._name is a dict we are still loading
|
|
678
|
+
# if its a string it was already complete
|
|
679
|
+
char_and_save = mdata["ModuleName"].split(":")
|
|
680
|
+
char = char_and_save[0]
|
|
681
|
+
self._name[int(char)] = chr(message.data)
|
|
682
|
+
if len(char_and_save) > 1 and char_and_save[1] == "Save":
|
|
683
|
+
self._name = "".join(
|
|
684
|
+
str(x) for x in self._name.values() if x != chr(0xFF)
|
|
685
|
+
)
|
|
686
|
+
elif "Match" in mdata:
|
|
687
|
+
for chan, chan_data in handle_match(mdata["Match"], message.data).items():
|
|
688
|
+
data = chan_data.copy()
|
|
689
|
+
await self._update_channel(chan, data)
|
|
690
|
+
elif "SensorName" in mdata:
|
|
691
|
+
# this is part of the channel names
|
|
692
|
+
# make sure we set the channel to loaded
|
|
693
|
+
# format of the value (in mdata)
|
|
694
|
+
# channel:char:start/save
|
|
695
|
+
spl = mdata["SensorName"].split(":")
|
|
696
|
+
if len(spl) == 2:
|
|
697
|
+
[chan, pos] = spl
|
|
698
|
+
elif len(spl) == 3:
|
|
699
|
+
[chan, pos, dummy] = spl
|
|
700
|
+
chan = self._translate_channel_name(chan)
|
|
701
|
+
self._channels[chan].set_name_char(pos, message.data)
|
|
702
|
+
else:
|
|
703
|
+
self._log.debug(mdata)
|
|
376
704
|
|
|
377
|
-
def _process_channel_name_message(self, part, message) -> None:
|
|
705
|
+
def _process_channel_name_message(self, part: int, message: Message) -> None:
|
|
378
706
|
channel = self._translate_channel_name(message.channel)
|
|
379
707
|
if channel not in self._channels:
|
|
380
708
|
return
|
|
@@ -393,7 +721,7 @@ class Module:
|
|
|
393
721
|
)
|
|
394
722
|
return int(channel)
|
|
395
723
|
|
|
396
|
-
def is_loaded(self) -> bool:
|
|
724
|
+
async def is_loaded(self) -> bool:
|
|
397
725
|
"""
|
|
398
726
|
Check if all name messages have been received
|
|
399
727
|
"""
|
|
@@ -411,14 +739,31 @@ class Module:
|
|
|
411
739
|
return False
|
|
412
740
|
# set that we finished the module loading
|
|
413
741
|
self.loaded = True
|
|
414
|
-
self._cache()
|
|
742
|
+
await self._cache()
|
|
415
743
|
return True
|
|
416
744
|
|
|
417
745
|
async def _request_module_status(self) -> None:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
746
|
+
"""Request current state of channels."""
|
|
747
|
+
if "Channels" not in self._data:
|
|
748
|
+
# some modules have no channels
|
|
749
|
+
return
|
|
750
|
+
self._log.info(f"Request module status {self._address}")
|
|
751
|
+
|
|
752
|
+
mod_stat_req_msg = ModuleStatusRequestMessage(self._address)
|
|
753
|
+
counter_msg = None
|
|
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))
|
|
764
|
+
await self._writer(mod_stat_req_msg)
|
|
765
|
+
if counter_msg is not None:
|
|
766
|
+
await self._writer(counter_msg)
|
|
422
767
|
|
|
423
768
|
async def _request_channel_name(self) -> None:
|
|
424
769
|
# request the module channel names
|
|
@@ -428,7 +773,10 @@ class Module:
|
|
|
428
773
|
msg.channels = 0xFF
|
|
429
774
|
await self._writer(msg)
|
|
430
775
|
else:
|
|
431
|
-
|
|
776
|
+
msg_type = commandRegistry.get_command(
|
|
777
|
+
CHANNEL_NAME_REQUEST_COMMAND_CODE, self.get_type()
|
|
778
|
+
)
|
|
779
|
+
msg = msg_type(self._address)
|
|
432
780
|
msg.priority = PRIORITY_LOW
|
|
433
781
|
msg.channels = list(range(1, (self.number_of_channels() + 1)))
|
|
434
782
|
await self._writer(msg)
|
|
@@ -441,9 +789,14 @@ class Module:
|
|
|
441
789
|
self._name = None
|
|
442
790
|
return
|
|
443
791
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
792
|
+
if self._type == 0x0C:
|
|
793
|
+
self._name = None
|
|
794
|
+
return
|
|
795
|
+
|
|
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():
|
|
447
800
|
addr = struct.unpack(
|
|
448
801
|
">BB", struct.pack(">h", int("0x" + addr_int, 0))
|
|
449
802
|
)
|
|
@@ -453,15 +806,184 @@ class Module:
|
|
|
453
806
|
msg.low_address = addr[1]
|
|
454
807
|
await self._writer(msg)
|
|
455
808
|
|
|
456
|
-
def
|
|
809
|
+
async def _load_default_channels(self) -> None:
|
|
457
810
|
if "Channels" not in self._data:
|
|
458
811
|
return
|
|
459
812
|
|
|
460
813
|
for chan, chan_data in self._data["Channels"].items():
|
|
461
814
|
edit = True
|
|
815
|
+
sub = True
|
|
462
816
|
if "Editable" not in chan_data or chan_data["Editable"] != "yes":
|
|
463
817
|
edit = False
|
|
818
|
+
if "Subdevice" not in chan_data or chan_data["Subdevice"] != "yes":
|
|
819
|
+
sub = False
|
|
464
820
|
cls = getattr(sys.modules[__name__], chan_data["Type"])
|
|
465
821
|
self._channels[int(chan)] = cls(
|
|
466
|
-
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,
|
|
829
|
+
)
|
|
830
|
+
if chan_data["Type"] == "Temperature":
|
|
831
|
+
if "Thermostat" in self._data or (
|
|
832
|
+
"ThermostatAddr" in self._data and self._data["ThermostatAddr"] != 0
|
|
833
|
+
):
|
|
834
|
+
await self._update_channel(int(chan), {"thermostat": True})
|
|
835
|
+
if chan_data["Type"] == "Dimmer" and "sliderScale" in self._data:
|
|
836
|
+
self._channels[int(chan)].slider_scale = self._data["sliderScale"]
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
class VmbDali(Module):
|
|
840
|
+
"""
|
|
841
|
+
DALI has a variable number of channels: the number of channels
|
|
842
|
+
depends on the number of DALI devices on the DALI bus
|
|
843
|
+
"""
|
|
844
|
+
|
|
845
|
+
def __init__(
|
|
846
|
+
self,
|
|
847
|
+
module_address: int,
|
|
848
|
+
module_type: int,
|
|
849
|
+
serial: int | None = None,
|
|
850
|
+
memorymap: int | None = None,
|
|
851
|
+
build_year: int | None = None,
|
|
852
|
+
build_week: int | None = None,
|
|
853
|
+
cache_dir: str | None = None,
|
|
854
|
+
) -> None:
|
|
855
|
+
super().__init__(
|
|
856
|
+
module_address,
|
|
857
|
+
module_type,
|
|
858
|
+
serial,
|
|
859
|
+
memorymap,
|
|
860
|
+
build_year,
|
|
861
|
+
build_week,
|
|
862
|
+
cache_dir,
|
|
863
|
+
)
|
|
864
|
+
self.group_members: dict[int, set[int]] = {}
|
|
865
|
+
|
|
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,
|
|
467
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()
|
|
884
|
+
|
|
885
|
+
async def _request_dali_channels(self):
|
|
886
|
+
msg_type = commandRegistry.get_command(
|
|
887
|
+
DALI_DEVICE_SETTINGS_REQUEST_COMMAND_CODE, self.get_type()
|
|
888
|
+
)
|
|
889
|
+
msg: DaliDeviceSettingsRequest = msg_type(self._address)
|
|
890
|
+
msg.priority = PRIORITY_LOW
|
|
891
|
+
msg.channel = 81 # all
|
|
892
|
+
msg.settings = None # all
|
|
893
|
+
await self._writer(msg)
|
|
894
|
+
|
|
895
|
+
async def on_message(self, message: Message) -> None:
|
|
896
|
+
if isinstance(message, DaliDeviceSettingMsg):
|
|
897
|
+
if isinstance(message.data, DaliDeviceTypeMsg):
|
|
898
|
+
if message.data.device_type == DaliDeviceType.NoDevicePresent:
|
|
899
|
+
if message.channel in self._channels:
|
|
900
|
+
del self._channels[message.channel]
|
|
901
|
+
elif message.data.device_type == DaliDeviceType.LedModule:
|
|
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:
|
|
921
|
+
# New or changed type, replace channel:
|
|
922
|
+
self._channels[message.channel] = Dimmer(
|
|
923
|
+
self,
|
|
924
|
+
message.channel,
|
|
925
|
+
None,
|
|
926
|
+
True,
|
|
927
|
+
True,
|
|
928
|
+
self._writer,
|
|
929
|
+
self._address,
|
|
930
|
+
slider_scale=254,
|
|
931
|
+
)
|
|
932
|
+
await self._request_single_channel_name(message.channel)
|
|
933
|
+
|
|
934
|
+
elif isinstance(message.data, MemberOfGroupMsg):
|
|
935
|
+
for group in range(0, 15 + 1):
|
|
936
|
+
this_group_members = self.group_members.setdefault(group, set())
|
|
937
|
+
if message.data.member_of_group[group]:
|
|
938
|
+
this_group_members.add(message.channel)
|
|
939
|
+
elif message.channel in this_group_members:
|
|
940
|
+
this_group_members.remove(message.channel)
|
|
941
|
+
|
|
942
|
+
elif isinstance(message, PushButtonStatusMessage):
|
|
943
|
+
_channel_offset = self.calc_channel_offset(message.address)
|
|
944
|
+
for channel in message.opened:
|
|
945
|
+
if _channel_offset + channel > 64: # ignore groups
|
|
946
|
+
continue
|
|
947
|
+
await self._update_channel((_channel_offset + channel), {"state": 0})
|
|
948
|
+
# ignore message.closed: we don't know at what dimlevel they're started
|
|
949
|
+
|
|
950
|
+
elif isinstance(message, DimValueStatus):
|
|
951
|
+
for offset, dim_value in enumerate(message.dim_values):
|
|
952
|
+
channel = message.channel + offset
|
|
953
|
+
if channel <= 64: # channel
|
|
954
|
+
await self._update_channel(channel, {"state": dim_value})
|
|
955
|
+
elif channel <= 80: # group
|
|
956
|
+
group_num = channel - 65
|
|
957
|
+
for chan in self.group_members.get(group_num, []):
|
|
958
|
+
await self._update_channel(chan, {"state": dim_value})
|
|
959
|
+
else: # broadcast
|
|
960
|
+
for chan in self._channels.values():
|
|
961
|
+
await chan.update({"state": dim_value})
|
|
962
|
+
|
|
963
|
+
elif isinstance(
|
|
964
|
+
message,
|
|
965
|
+
(
|
|
966
|
+
SetLedMessage,
|
|
967
|
+
ClearLedMessage,
|
|
968
|
+
FastBlinkingLedMessage,
|
|
969
|
+
SlowBlinkingLedMessage,
|
|
970
|
+
),
|
|
971
|
+
):
|
|
972
|
+
pass
|
|
973
|
+
|
|
974
|
+
else:
|
|
975
|
+
return await super().on_message(message)
|
|
976
|
+
|
|
977
|
+
async def _request_channel_name(self) -> None:
|
|
978
|
+
# Channel names are requested after channel scan
|
|
979
|
+
# don't do them here (at initialization time)
|
|
980
|
+
pass
|
|
981
|
+
|
|
982
|
+
async def _request_single_channel_name(self, channel_num: int) -> None:
|
|
983
|
+
msg_type = commandRegistry.get_command(
|
|
984
|
+
CHANNEL_NAME_REQUEST_COMMAND_CODE, self.get_type()
|
|
985
|
+
)
|
|
986
|
+
msg = msg_type(self._address)
|
|
987
|
+
msg.priority = PRIORITY_LOW
|
|
988
|
+
msg.channels = channel_num
|
|
989
|
+
await self._writer(msg)
|