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.

Files changed (77) hide show
  1. aiohomematic/__init__.py +61 -0
  2. aiohomematic/async_support.py +212 -0
  3. aiohomematic/central/__init__.py +2309 -0
  4. aiohomematic/central/decorators.py +155 -0
  5. aiohomematic/central/rpc_server.py +295 -0
  6. aiohomematic/client/__init__.py +1848 -0
  7. aiohomematic/client/_rpc_errors.py +81 -0
  8. aiohomematic/client/json_rpc.py +1326 -0
  9. aiohomematic/client/rpc_proxy.py +311 -0
  10. aiohomematic/const.py +1127 -0
  11. aiohomematic/context.py +18 -0
  12. aiohomematic/converter.py +108 -0
  13. aiohomematic/decorators.py +302 -0
  14. aiohomematic/exceptions.py +164 -0
  15. aiohomematic/hmcli.py +186 -0
  16. aiohomematic/model/__init__.py +140 -0
  17. aiohomematic/model/calculated/__init__.py +84 -0
  18. aiohomematic/model/calculated/climate.py +290 -0
  19. aiohomematic/model/calculated/data_point.py +327 -0
  20. aiohomematic/model/calculated/operating_voltage_level.py +299 -0
  21. aiohomematic/model/calculated/support.py +234 -0
  22. aiohomematic/model/custom/__init__.py +177 -0
  23. aiohomematic/model/custom/climate.py +1532 -0
  24. aiohomematic/model/custom/cover.py +792 -0
  25. aiohomematic/model/custom/data_point.py +334 -0
  26. aiohomematic/model/custom/definition.py +871 -0
  27. aiohomematic/model/custom/light.py +1128 -0
  28. aiohomematic/model/custom/lock.py +394 -0
  29. aiohomematic/model/custom/siren.py +275 -0
  30. aiohomematic/model/custom/support.py +41 -0
  31. aiohomematic/model/custom/switch.py +175 -0
  32. aiohomematic/model/custom/valve.py +114 -0
  33. aiohomematic/model/data_point.py +1123 -0
  34. aiohomematic/model/device.py +1445 -0
  35. aiohomematic/model/event.py +208 -0
  36. aiohomematic/model/generic/__init__.py +217 -0
  37. aiohomematic/model/generic/action.py +34 -0
  38. aiohomematic/model/generic/binary_sensor.py +30 -0
  39. aiohomematic/model/generic/button.py +27 -0
  40. aiohomematic/model/generic/data_point.py +171 -0
  41. aiohomematic/model/generic/dummy.py +147 -0
  42. aiohomematic/model/generic/number.py +76 -0
  43. aiohomematic/model/generic/select.py +39 -0
  44. aiohomematic/model/generic/sensor.py +74 -0
  45. aiohomematic/model/generic/switch.py +54 -0
  46. aiohomematic/model/generic/text.py +29 -0
  47. aiohomematic/model/hub/__init__.py +333 -0
  48. aiohomematic/model/hub/binary_sensor.py +24 -0
  49. aiohomematic/model/hub/button.py +28 -0
  50. aiohomematic/model/hub/data_point.py +340 -0
  51. aiohomematic/model/hub/number.py +39 -0
  52. aiohomematic/model/hub/select.py +49 -0
  53. aiohomematic/model/hub/sensor.py +37 -0
  54. aiohomematic/model/hub/switch.py +44 -0
  55. aiohomematic/model/hub/text.py +30 -0
  56. aiohomematic/model/support.py +586 -0
  57. aiohomematic/model/update.py +143 -0
  58. aiohomematic/property_decorators.py +496 -0
  59. aiohomematic/py.typed +0 -0
  60. aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
  61. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  62. aiohomematic/rega_scripts/get_serial.fn +44 -0
  63. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  64. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  65. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  66. aiohomematic/store/__init__.py +34 -0
  67. aiohomematic/store/dynamic.py +551 -0
  68. aiohomematic/store/persistent.py +988 -0
  69. aiohomematic/store/visibility.py +812 -0
  70. aiohomematic/support.py +664 -0
  71. aiohomematic/validator.py +112 -0
  72. aiohomematic-2025.11.3.dist-info/METADATA +144 -0
  73. aiohomematic-2025.11.3.dist-info/RECORD +77 -0
  74. aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
  75. aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
  76. aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
  77. 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