aiohomematic 2025.11.3__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.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/__init__.py +61 -0
- aiohomematic/async_support.py +212 -0
- aiohomematic/central/__init__.py +2309 -0
- aiohomematic/central/decorators.py +155 -0
- aiohomematic/central/rpc_server.py +295 -0
- aiohomematic/client/__init__.py +1848 -0
- aiohomematic/client/_rpc_errors.py +81 -0
- aiohomematic/client/json_rpc.py +1326 -0
- aiohomematic/client/rpc_proxy.py +311 -0
- aiohomematic/const.py +1127 -0
- aiohomematic/context.py +18 -0
- aiohomematic/converter.py +108 -0
- aiohomematic/decorators.py +302 -0
- aiohomematic/exceptions.py +164 -0
- aiohomematic/hmcli.py +186 -0
- aiohomematic/model/__init__.py +140 -0
- aiohomematic/model/calculated/__init__.py +84 -0
- aiohomematic/model/calculated/climate.py +290 -0
- aiohomematic/model/calculated/data_point.py +327 -0
- aiohomematic/model/calculated/operating_voltage_level.py +299 -0
- aiohomematic/model/calculated/support.py +234 -0
- aiohomematic/model/custom/__init__.py +177 -0
- aiohomematic/model/custom/climate.py +1532 -0
- aiohomematic/model/custom/cover.py +792 -0
- aiohomematic/model/custom/data_point.py +334 -0
- aiohomematic/model/custom/definition.py +871 -0
- aiohomematic/model/custom/light.py +1128 -0
- aiohomematic/model/custom/lock.py +394 -0
- aiohomematic/model/custom/siren.py +275 -0
- aiohomematic/model/custom/support.py +41 -0
- aiohomematic/model/custom/switch.py +175 -0
- aiohomematic/model/custom/valve.py +114 -0
- aiohomematic/model/data_point.py +1123 -0
- aiohomematic/model/device.py +1445 -0
- aiohomematic/model/event.py +208 -0
- aiohomematic/model/generic/__init__.py +217 -0
- aiohomematic/model/generic/action.py +34 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +27 -0
- aiohomematic/model/generic/data_point.py +171 -0
- aiohomematic/model/generic/dummy.py +147 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +39 -0
- aiohomematic/model/generic/sensor.py +74 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +29 -0
- aiohomematic/model/hub/__init__.py +333 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/data_point.py +340 -0
- aiohomematic/model/hub/number.py +39 -0
- aiohomematic/model/hub/select.py +49 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +44 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/support.py +586 -0
- aiohomematic/model/update.py +143 -0
- aiohomematic/property_decorators.py +496 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
- aiohomematic/rega_scripts/set_program_state.fn +12 -0
- aiohomematic/rega_scripts/set_system_variable.fn +15 -0
- aiohomematic/store/__init__.py +34 -0
- aiohomematic/store/dynamic.py +551 -0
- aiohomematic/store/persistent.py +988 -0
- aiohomematic/store/visibility.py +812 -0
- aiohomematic/support.py +664 -0
- aiohomematic/validator.py +112 -0
- aiohomematic-2025.11.3.dist-info/METADATA +144 -0
- aiohomematic-2025.11.3.dist-info/RECORD +77 -0
- aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
- aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
- aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2025.11.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""Support for data points used within aiohomematic."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import abstractmethod
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Final
|
|
13
|
+
|
|
14
|
+
from aiohomematic import central as hmcu
|
|
15
|
+
from aiohomematic.const import (
|
|
16
|
+
ADDRESS_SEPARATOR,
|
|
17
|
+
CDPD,
|
|
18
|
+
PROGRAM_ADDRESS,
|
|
19
|
+
PROGRAM_SET_PATH_ROOT,
|
|
20
|
+
PROGRAM_STATE_PATH_ROOT,
|
|
21
|
+
SET_PATH_ROOT,
|
|
22
|
+
STATE_PATH_ROOT,
|
|
23
|
+
SYSVAR_ADDRESS,
|
|
24
|
+
SYSVAR_SET_PATH_ROOT,
|
|
25
|
+
SYSVAR_STATE_PATH_ROOT,
|
|
26
|
+
SYSVAR_TYPE,
|
|
27
|
+
VIRTDEV_SET_PATH_ROOT,
|
|
28
|
+
VIRTDEV_STATE_PATH_ROOT,
|
|
29
|
+
VIRTUAL_REMOTE_ADDRESSES,
|
|
30
|
+
DataPointUsage,
|
|
31
|
+
Interface,
|
|
32
|
+
ParameterData,
|
|
33
|
+
ParameterType,
|
|
34
|
+
)
|
|
35
|
+
from aiohomematic.model import device as hmd
|
|
36
|
+
from aiohomematic.support import to_bool
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"ChannelNameData",
|
|
40
|
+
"DataPointNameData",
|
|
41
|
+
"GenericParameterType",
|
|
42
|
+
"check_channel_is_the_only_primary_channel",
|
|
43
|
+
"convert_value",
|
|
44
|
+
"generate_channel_unique_id",
|
|
45
|
+
"generate_unique_id",
|
|
46
|
+
"get_channel_name_data",
|
|
47
|
+
"get_custom_data_point_name",
|
|
48
|
+
"get_device_name",
|
|
49
|
+
"get_data_point_name_data",
|
|
50
|
+
"get_event_name",
|
|
51
|
+
"get_index_of_value_from_value_list",
|
|
52
|
+
"get_value_from_value_list",
|
|
53
|
+
"is_binary_sensor",
|
|
54
|
+
]
|
|
55
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
type GenericParameterType = bool | int | float | str | None
|
|
58
|
+
|
|
59
|
+
# dict with binary_sensor relevant value lists and the corresponding TRUE value
|
|
60
|
+
_BINARY_SENSOR_TRUE_VALUE_DICT_FOR_VALUE_LIST: Final[Mapping[tuple[str, ...], str]] = {
|
|
61
|
+
("CLOSED", "OPEN"): "OPEN",
|
|
62
|
+
("DRY", "RAIN"): "RAIN",
|
|
63
|
+
("STABLE", "NOT_STABLE"): "NOT_STABLE",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ChannelNameData:
|
|
68
|
+
"""Dataclass for channel name parts."""
|
|
69
|
+
|
|
70
|
+
__slots__ = (
|
|
71
|
+
"channel_name",
|
|
72
|
+
"device_name",
|
|
73
|
+
"full_name",
|
|
74
|
+
"sub_device_name",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def __init__(self, *, device_name: str, channel_name: str) -> None:
|
|
78
|
+
"""Init the DataPointNameData class."""
|
|
79
|
+
self.device_name: Final = device_name
|
|
80
|
+
self.channel_name: Final = self._get_channel_name(device_name=device_name, channel_name=channel_name)
|
|
81
|
+
self.full_name = f"{device_name} {self.channel_name}".strip() if self.channel_name else device_name
|
|
82
|
+
self.sub_device_name = channel_name if channel_name else device_name
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def empty() -> ChannelNameData:
|
|
86
|
+
"""Return an empty DataPointNameData."""
|
|
87
|
+
return ChannelNameData(device_name="", channel_name="")
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _get_channel_name(*, device_name: str, channel_name: str) -> str:
|
|
91
|
+
"""Return the channel_name of the data_point only name."""
|
|
92
|
+
if device_name and channel_name and channel_name.startswith(device_name):
|
|
93
|
+
c_name = channel_name.replace(device_name, "").strip()
|
|
94
|
+
if c_name.startswith(ADDRESS_SEPARATOR):
|
|
95
|
+
c_name = c_name[1:]
|
|
96
|
+
return c_name
|
|
97
|
+
return channel_name.strip()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class DataPointNameData(ChannelNameData):
|
|
101
|
+
"""Dataclass for data_point name parts."""
|
|
102
|
+
|
|
103
|
+
__slots__ = (
|
|
104
|
+
"name",
|
|
105
|
+
"parameter_name",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def __init__(self, *, device_name: str, channel_name: str, parameter_name: str | None = None) -> None:
|
|
109
|
+
"""Init the DataPointNameData class."""
|
|
110
|
+
super().__init__(device_name=device_name, channel_name=channel_name)
|
|
111
|
+
|
|
112
|
+
self.name: Final = self._get_data_point_name(
|
|
113
|
+
device_name=device_name, channel_name=channel_name, parameter_name=parameter_name
|
|
114
|
+
)
|
|
115
|
+
self.full_name = f"{device_name} {self.name}".strip() if self.name else device_name
|
|
116
|
+
self.parameter_name = parameter_name
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def empty() -> DataPointNameData:
|
|
120
|
+
"""Return an empty DataPointNameData."""
|
|
121
|
+
return DataPointNameData(device_name="", channel_name="")
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _get_channel_parameter_name(*, channel_name: str, parameter_name: str | None) -> str:
|
|
125
|
+
"""Return the channel parameter name of the data_point."""
|
|
126
|
+
if channel_name and parameter_name:
|
|
127
|
+
return f"{channel_name} {parameter_name}".strip()
|
|
128
|
+
return channel_name.strip()
|
|
129
|
+
|
|
130
|
+
def _get_data_point_name(self, *, device_name: str, channel_name: str, parameter_name: str | None) -> str:
|
|
131
|
+
"""Return the name of the data_point only name."""
|
|
132
|
+
channel_parameter_name = self._get_channel_parameter_name(
|
|
133
|
+
channel_name=channel_name, parameter_name=parameter_name
|
|
134
|
+
)
|
|
135
|
+
if device_name and channel_parameter_name and channel_parameter_name.startswith(device_name):
|
|
136
|
+
return channel_parameter_name[len(device_name) :].lstrip()
|
|
137
|
+
return channel_parameter_name
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class HubNameData:
|
|
141
|
+
"""Class for hub data_point name parts."""
|
|
142
|
+
|
|
143
|
+
__slots__ = (
|
|
144
|
+
"full_name",
|
|
145
|
+
"name",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def __init__(self, *, name: str, central_name: str | None = None, channel_name: str | None = None) -> None:
|
|
149
|
+
"""Init the DataPointNameData class."""
|
|
150
|
+
self.name: Final = name
|
|
151
|
+
self.full_name = (
|
|
152
|
+
f"{channel_name} {self.name}".strip() if channel_name else f"{central_name} {self.name}".strip()
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def empty() -> HubNameData:
|
|
157
|
+
"""Return an empty HubNameData."""
|
|
158
|
+
return HubNameData(name="")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def check_length_and_log(name: str | None, value: Any) -> Any:
|
|
162
|
+
"""Check the length of a datapoint and log if too long."""
|
|
163
|
+
if isinstance(value, str) and len(value) > 255:
|
|
164
|
+
_LOGGER.debug(
|
|
165
|
+
"Value of datapoint %s exceedes maximum allowed length of 255 chars. Value will be limited to 255 chars",
|
|
166
|
+
name,
|
|
167
|
+
)
|
|
168
|
+
return value[0:255:1]
|
|
169
|
+
return value
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_device_name(central: hmcu.CentralUnit, device_address: str, model: str) -> str:
|
|
173
|
+
"""Return the cached name for a device, or an auto-generated."""
|
|
174
|
+
if name := central.device_details.get_name(address=device_address):
|
|
175
|
+
return name
|
|
176
|
+
|
|
177
|
+
_LOGGER.debug(
|
|
178
|
+
"GET_DEVICE_NAME: Using auto-generated name for %s %s",
|
|
179
|
+
model,
|
|
180
|
+
device_address,
|
|
181
|
+
)
|
|
182
|
+
return _get_generic_name(address=device_address, model=model)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _get_generic_name(address: str, model: str) -> str:
|
|
186
|
+
"""Return auto-generated device/channel name."""
|
|
187
|
+
return f"{model}_{address}"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_channel_name_data(channel: hmd.Channel) -> ChannelNameData:
|
|
191
|
+
"""Get name for data_point."""
|
|
192
|
+
if channel_base_name := _get_base_name_from_channel_or_device(channel=channel):
|
|
193
|
+
return ChannelNameData(
|
|
194
|
+
device_name=channel.device.name,
|
|
195
|
+
channel_name=channel_base_name,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
_LOGGER.debug(
|
|
199
|
+
"GET_CHANNEL_NAME_DATA: Using unique_id for %s %s",
|
|
200
|
+
channel.device.model,
|
|
201
|
+
channel.address,
|
|
202
|
+
)
|
|
203
|
+
return ChannelNameData.empty()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class PathData:
|
|
207
|
+
"""The data point path data."""
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
@abstractmethod
|
|
211
|
+
def set_path(self) -> str:
|
|
212
|
+
"""Return the base set path of the data_point."""
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
@abstractmethod
|
|
216
|
+
def state_path(self) -> str:
|
|
217
|
+
"""Return the base state path of the data_point."""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class DataPointPathData(PathData):
|
|
221
|
+
"""The data point path data."""
|
|
222
|
+
|
|
223
|
+
__slots__ = (
|
|
224
|
+
"_set_path",
|
|
225
|
+
"_state_path",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def __init__(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
interface: Interface | None,
|
|
232
|
+
address: str,
|
|
233
|
+
channel_no: int | None,
|
|
234
|
+
kind: str,
|
|
235
|
+
name: str | None = None,
|
|
236
|
+
):
|
|
237
|
+
"""Init the path data."""
|
|
238
|
+
path_item: Final = f"{address.upper()}/{channel_no}/{kind.upper()}"
|
|
239
|
+
self._set_path: Final = (
|
|
240
|
+
f"{VIRTDEV_SET_PATH_ROOT if interface == Interface.CCU_JACK else SET_PATH_ROOT}/{path_item}"
|
|
241
|
+
)
|
|
242
|
+
self._state_path: Final = (
|
|
243
|
+
f"{VIRTDEV_STATE_PATH_ROOT if interface == Interface.CCU_JACK else STATE_PATH_ROOT}/{path_item}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def set_path(self) -> str:
|
|
248
|
+
"""Return the base set path of the data_point."""
|
|
249
|
+
return self._set_path
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def state_path(self) -> str:
|
|
253
|
+
"""Return the base state path of the data_point."""
|
|
254
|
+
return self._state_path
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class ProgramPathData(PathData):
|
|
258
|
+
"""The program path data."""
|
|
259
|
+
|
|
260
|
+
__slots__ = (
|
|
261
|
+
"_set_path",
|
|
262
|
+
"_state_path",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def __init__(self, *, pid: str):
|
|
266
|
+
"""Init the path data."""
|
|
267
|
+
self._set_path: Final = f"{PROGRAM_SET_PATH_ROOT}/{pid}"
|
|
268
|
+
self._state_path: Final = f"{PROGRAM_STATE_PATH_ROOT}/{pid}"
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def set_path(self) -> str:
|
|
272
|
+
"""Return the base set path of the program."""
|
|
273
|
+
return self._set_path
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def state_path(self) -> str:
|
|
277
|
+
"""Return the base state path of the program."""
|
|
278
|
+
return self._state_path
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class SysvarPathData(PathData):
|
|
282
|
+
"""The sysvar path data."""
|
|
283
|
+
|
|
284
|
+
__slots__ = (
|
|
285
|
+
"_set_path",
|
|
286
|
+
"_state_path",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def __init__(self, *, vid: str):
|
|
290
|
+
"""Init the path data."""
|
|
291
|
+
self._set_path: Final = f"{SYSVAR_SET_PATH_ROOT}/{vid}"
|
|
292
|
+
self._state_path: Final = f"{SYSVAR_STATE_PATH_ROOT}/{vid}"
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def set_path(self) -> str:
|
|
296
|
+
"""Return the base set path of the sysvar."""
|
|
297
|
+
return self._set_path
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def state_path(self) -> str:
|
|
301
|
+
"""Return the base state path of the sysvar."""
|
|
302
|
+
return self._state_path
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def get_data_point_name_data(
|
|
306
|
+
channel: hmd.Channel,
|
|
307
|
+
parameter: str,
|
|
308
|
+
) -> DataPointNameData:
|
|
309
|
+
"""Get name for data_point."""
|
|
310
|
+
if channel_name := _get_base_name_from_channel_or_device(channel=channel):
|
|
311
|
+
p_name = parameter.title().replace("_", " ")
|
|
312
|
+
|
|
313
|
+
if _check_channel_name_with_channel_no(name=channel_name):
|
|
314
|
+
c_name = channel_name.split(ADDRESS_SEPARATOR)[0]
|
|
315
|
+
c_postfix = ""
|
|
316
|
+
if channel.central.paramset_descriptions.is_in_multiple_channels(
|
|
317
|
+
channel_address=channel.address, parameter=parameter
|
|
318
|
+
):
|
|
319
|
+
c_postfix = "" if channel.no in (0, None) else f" ch{channel.no}"
|
|
320
|
+
data_point_name = DataPointNameData(
|
|
321
|
+
device_name=channel.device.name,
|
|
322
|
+
channel_name=c_name,
|
|
323
|
+
parameter_name=f"{p_name}{c_postfix}",
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
data_point_name = DataPointNameData(
|
|
327
|
+
device_name=channel.device.name,
|
|
328
|
+
channel_name=channel_name,
|
|
329
|
+
parameter_name=p_name,
|
|
330
|
+
)
|
|
331
|
+
return data_point_name
|
|
332
|
+
|
|
333
|
+
_LOGGER.debug(
|
|
334
|
+
"GET_DATA_POINT_NAME: Using unique_id for %s %s %s",
|
|
335
|
+
channel.device.model,
|
|
336
|
+
channel.address,
|
|
337
|
+
parameter,
|
|
338
|
+
)
|
|
339
|
+
return DataPointNameData.empty()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def get_hub_data_point_name_data(
|
|
343
|
+
channel: hmd.Channel | None,
|
|
344
|
+
legacy_name: str,
|
|
345
|
+
central_name: str,
|
|
346
|
+
) -> HubNameData:
|
|
347
|
+
"""Get name for hub data_point."""
|
|
348
|
+
if not channel:
|
|
349
|
+
return HubNameData(
|
|
350
|
+
central_name=central_name,
|
|
351
|
+
name=legacy_name,
|
|
352
|
+
)
|
|
353
|
+
if channel_name := _get_base_name_from_channel_or_device(channel=channel):
|
|
354
|
+
p_name = (
|
|
355
|
+
legacy_name.replace("_", " ")
|
|
356
|
+
.replace(channel.address, "")
|
|
357
|
+
.replace(channel.id, "")
|
|
358
|
+
.replace(channel.device.id, "")
|
|
359
|
+
.strip()
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if _check_channel_name_with_channel_no(name=channel_name):
|
|
363
|
+
channel_name = channel_name.split(":")[0]
|
|
364
|
+
|
|
365
|
+
return HubNameData(channel_name=channel_name, name=p_name)
|
|
366
|
+
|
|
367
|
+
_LOGGER.debug(
|
|
368
|
+
"GET_DATA_POINT_NAME: Using unique_id for %s %s %s",
|
|
369
|
+
channel.device.model,
|
|
370
|
+
channel.address,
|
|
371
|
+
legacy_name,
|
|
372
|
+
)
|
|
373
|
+
return HubNameData.empty()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def get_event_name(
|
|
377
|
+
channel: hmd.Channel,
|
|
378
|
+
parameter: str,
|
|
379
|
+
) -> DataPointNameData:
|
|
380
|
+
"""Get name for event."""
|
|
381
|
+
if channel_name := _get_base_name_from_channel_or_device(channel=channel):
|
|
382
|
+
p_name = parameter.title().replace("_", " ")
|
|
383
|
+
if _check_channel_name_with_channel_no(name=channel_name):
|
|
384
|
+
c_name = "" if channel.no in (0, None) else f" ch{channel.no}"
|
|
385
|
+
event_name = DataPointNameData(
|
|
386
|
+
device_name=channel.device.name,
|
|
387
|
+
channel_name=c_name,
|
|
388
|
+
parameter_name=p_name,
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
event_name = DataPointNameData(
|
|
392
|
+
device_name=channel.device.name,
|
|
393
|
+
channel_name=channel_name,
|
|
394
|
+
parameter_name=p_name,
|
|
395
|
+
)
|
|
396
|
+
return event_name
|
|
397
|
+
|
|
398
|
+
_LOGGER.debug(
|
|
399
|
+
"GET_EVENT_NAME: Using unique_id for %s %s %s",
|
|
400
|
+
channel.device.model,
|
|
401
|
+
channel.address,
|
|
402
|
+
parameter,
|
|
403
|
+
)
|
|
404
|
+
return DataPointNameData.empty()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def get_custom_data_point_name(
|
|
408
|
+
channel: hmd.Channel,
|
|
409
|
+
is_only_primary_channel: bool,
|
|
410
|
+
ignore_multiple_channels_for_name: bool,
|
|
411
|
+
usage: DataPointUsage,
|
|
412
|
+
postfix: str = "",
|
|
413
|
+
) -> DataPointNameData:
|
|
414
|
+
"""Get name for custom data_point."""
|
|
415
|
+
if channel_name := _get_base_name_from_channel_or_device(channel=channel):
|
|
416
|
+
if (is_only_primary_channel or ignore_multiple_channels_for_name) and _check_channel_name_with_channel_no(
|
|
417
|
+
name=channel_name
|
|
418
|
+
):
|
|
419
|
+
return DataPointNameData(
|
|
420
|
+
device_name=channel.device.name,
|
|
421
|
+
channel_name=channel_name.split(ADDRESS_SEPARATOR)[0],
|
|
422
|
+
parameter_name=postfix,
|
|
423
|
+
)
|
|
424
|
+
if _check_channel_name_with_channel_no(name=channel_name):
|
|
425
|
+
c_name = channel_name.split(ADDRESS_SEPARATOR)[0]
|
|
426
|
+
p_name = channel_name.split(ADDRESS_SEPARATOR)[1]
|
|
427
|
+
marker = "ch" if usage == DataPointUsage.CDP_PRIMARY else "vch"
|
|
428
|
+
p_name = f"{marker}{p_name}"
|
|
429
|
+
return DataPointNameData(device_name=channel.device.name, channel_name=c_name, parameter_name=p_name)
|
|
430
|
+
return DataPointNameData(device_name=channel.device.name, channel_name=channel_name)
|
|
431
|
+
|
|
432
|
+
_LOGGER.debug(
|
|
433
|
+
"GET_CUSTOM_DATA_POINT_NAME: Using unique_id for %s %s %s",
|
|
434
|
+
channel.device.model,
|
|
435
|
+
channel.address,
|
|
436
|
+
channel.no,
|
|
437
|
+
)
|
|
438
|
+
return DataPointNameData.empty()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def generate_unique_id(
|
|
442
|
+
central: hmcu.CentralUnit,
|
|
443
|
+
address: str,
|
|
444
|
+
parameter: str | None = None,
|
|
445
|
+
prefix: str | None = None,
|
|
446
|
+
) -> str:
|
|
447
|
+
"""
|
|
448
|
+
Build unique identifier from address and parameter.
|
|
449
|
+
|
|
450
|
+
Central id is additionally used for heating groups.
|
|
451
|
+
Prefix is used for events and buttons.
|
|
452
|
+
"""
|
|
453
|
+
unique_id = address.replace(ADDRESS_SEPARATOR, "_").replace("-", "_")
|
|
454
|
+
if parameter:
|
|
455
|
+
unique_id = f"{unique_id}_{parameter}"
|
|
456
|
+
|
|
457
|
+
if prefix:
|
|
458
|
+
unique_id = f"{prefix}_{unique_id}"
|
|
459
|
+
if (
|
|
460
|
+
address in (PROGRAM_ADDRESS, SYSVAR_ADDRESS)
|
|
461
|
+
or address.startswith("INT000")
|
|
462
|
+
or address.split(ADDRESS_SEPARATOR)[0] in VIRTUAL_REMOTE_ADDRESSES
|
|
463
|
+
):
|
|
464
|
+
return f"{central.config.central_id}_{unique_id}".lower()
|
|
465
|
+
return f"{unique_id}".lower()
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def generate_channel_unique_id(
|
|
469
|
+
central: hmcu.CentralUnit,
|
|
470
|
+
address: str,
|
|
471
|
+
) -> str:
|
|
472
|
+
"""Build unique identifier for a channel from address."""
|
|
473
|
+
unique_id = address.replace(ADDRESS_SEPARATOR, "_").replace("-", "_")
|
|
474
|
+
if address.split(ADDRESS_SEPARATOR)[0] in VIRTUAL_REMOTE_ADDRESSES:
|
|
475
|
+
return f"{central.config.central_id}_{unique_id}".lower()
|
|
476
|
+
return unique_id.lower()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _get_base_name_from_channel_or_device(channel: hmd.Channel) -> str | None:
|
|
480
|
+
"""Get the name from channel if it's not default, otherwise from device."""
|
|
481
|
+
default_channel_name = f"{channel.device.model} {channel.address}"
|
|
482
|
+
name = channel.central.device_details.get_name(address=channel.address)
|
|
483
|
+
if name is None or name == default_channel_name:
|
|
484
|
+
return channel.device.name if channel.no is None else f"{channel.device.name}:{channel.no}"
|
|
485
|
+
return name
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _check_channel_name_with_channel_no(name: str) -> bool:
|
|
489
|
+
"""Check if name contains channel and this is an int."""
|
|
490
|
+
if name.count(ADDRESS_SEPARATOR) == 1:
|
|
491
|
+
channel_part = name.split(ADDRESS_SEPARATOR)[1]
|
|
492
|
+
try:
|
|
493
|
+
int(channel_part)
|
|
494
|
+
except ValueError:
|
|
495
|
+
return False
|
|
496
|
+
return True
|
|
497
|
+
return False
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def convert_value(value: Any, target_type: ParameterType, value_list: tuple[str, ...] | None) -> Any:
|
|
501
|
+
"""
|
|
502
|
+
Convert a value to target_type with safe memoization.
|
|
503
|
+
|
|
504
|
+
To avoid redundant conversions across layers, we use an internal
|
|
505
|
+
LRU-cached helper for hashable inputs. For unhashable inputs, we
|
|
506
|
+
fall back to a direct conversion path.
|
|
507
|
+
"""
|
|
508
|
+
# Normalize value_list to tuple to ensure hashability where possible
|
|
509
|
+
norm_value_list: tuple[str, ...] | None = tuple(value_list) if isinstance(value_list, list) else value_list
|
|
510
|
+
try:
|
|
511
|
+
# This will be cached if all arguments are hashable
|
|
512
|
+
return _convert_value_cached(value, target_type, norm_value_list)
|
|
513
|
+
except TypeError:
|
|
514
|
+
# Fallback non-cached path if any argument is unhashable
|
|
515
|
+
return _convert_value_noncached(value, target_type, norm_value_list)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@lru_cache(maxsize=2048)
|
|
519
|
+
def _convert_value_cached(value: Any, target_type: ParameterType, value_list: tuple[str, ...] | None) -> Any:
|
|
520
|
+
return _convert_value_noncached(value, target_type, value_list)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _convert_value_noncached(value: Any, target_type: ParameterType, value_list: tuple[str, ...] | None) -> Any:
|
|
524
|
+
if value is None:
|
|
525
|
+
return None
|
|
526
|
+
if target_type == ParameterType.BOOL:
|
|
527
|
+
if value_list:
|
|
528
|
+
# relevant for ENUMs retyped to a BOOL
|
|
529
|
+
return _get_binary_sensor_value(value=value, value_list=value_list)
|
|
530
|
+
if isinstance(value, str):
|
|
531
|
+
return to_bool(value=value)
|
|
532
|
+
return bool(value)
|
|
533
|
+
if target_type == ParameterType.FLOAT:
|
|
534
|
+
return float(value)
|
|
535
|
+
if target_type == ParameterType.INTEGER:
|
|
536
|
+
return int(float(value))
|
|
537
|
+
if target_type == ParameterType.STRING:
|
|
538
|
+
return str(value)
|
|
539
|
+
return value
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def is_binary_sensor(parameter_data: ParameterData) -> bool:
|
|
543
|
+
"""Check, if the sensor is a binary_sensor."""
|
|
544
|
+
if parameter_data["TYPE"] == ParameterType.BOOL:
|
|
545
|
+
return True
|
|
546
|
+
if value_list := parameter_data.get("VALUE_LIST"):
|
|
547
|
+
return tuple(value_list) in _BINARY_SENSOR_TRUE_VALUE_DICT_FOR_VALUE_LIST
|
|
548
|
+
return False
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _get_binary_sensor_value(value: int, value_list: tuple[str, ...]) -> bool:
|
|
552
|
+
"""Return, the value of a binary_sensor."""
|
|
553
|
+
try:
|
|
554
|
+
str_value = value_list[value]
|
|
555
|
+
if true_value := _BINARY_SENSOR_TRUE_VALUE_DICT_FOR_VALUE_LIST.get(value_list):
|
|
556
|
+
return str_value == true_value
|
|
557
|
+
except IndexError:
|
|
558
|
+
pass
|
|
559
|
+
return False
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def check_channel_is_the_only_primary_channel(
|
|
563
|
+
current_channel_no: int | None,
|
|
564
|
+
device_def: Mapping[str, Any],
|
|
565
|
+
device_has_multiple_channels: bool,
|
|
566
|
+
) -> bool:
|
|
567
|
+
"""Check if this channel is the only primary channel."""
|
|
568
|
+
primary_channel: int = device_def[CDPD.PRIMARY_CHANNEL]
|
|
569
|
+
return bool(primary_channel == current_channel_no and device_has_multiple_channels is False)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def get_value_from_value_list(value: SYSVAR_TYPE, value_list: tuple[str, ...] | list[str] | None) -> str | None:
|
|
573
|
+
"""Check if value is in value list."""
|
|
574
|
+
if value is not None and isinstance(value, int) and value_list is not None and value < len(value_list):
|
|
575
|
+
return value_list[int(value)]
|
|
576
|
+
return None
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def get_index_of_value_from_value_list(
|
|
580
|
+
value: SYSVAR_TYPE, value_list: tuple[str, ...] | list[str] | None
|
|
581
|
+
) -> int | None:
|
|
582
|
+
"""Check if value is in value list."""
|
|
583
|
+
if value is not None and isinstance(value, str | StrEnum) and value_list is not None and value in value_list:
|
|
584
|
+
return value_list.index(value)
|
|
585
|
+
|
|
586
|
+
return None
|