aiohomematic 2025.8.9__py3-none-any.whl → 2025.9.1__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 +15 -1
- aiohomematic/async_support.py +15 -2
- aiohomematic/caches/__init__.py +2 -0
- aiohomematic/caches/dynamic.py +2 -0
- aiohomematic/caches/persistent.py +2 -0
- aiohomematic/caches/visibility.py +2 -0
- aiohomematic/central/__init__.py +43 -18
- aiohomematic/central/decorators.py +60 -15
- aiohomematic/central/xml_rpc_server.py +15 -1
- aiohomematic/client/__init__.py +2 -0
- aiohomematic/client/_rpc_errors.py +81 -0
- aiohomematic/client/json_rpc.py +68 -19
- aiohomematic/client/xml_rpc.py +15 -8
- aiohomematic/const.py +44 -3
- aiohomematic/context.py +11 -1
- aiohomematic/converter.py +27 -1
- aiohomematic/decorators.py +98 -25
- aiohomematic/exceptions.py +19 -1
- aiohomematic/hmcli.py +13 -1
- aiohomematic/model/__init__.py +2 -0
- aiohomematic/model/calculated/__init__.py +2 -0
- aiohomematic/model/calculated/climate.py +2 -0
- aiohomematic/model/calculated/data_point.py +2 -0
- aiohomematic/model/calculated/operating_voltage_level.py +2 -0
- aiohomematic/model/calculated/support.py +2 -0
- aiohomematic/model/custom/__init__.py +2 -0
- aiohomematic/model/custom/climate.py +3 -1
- aiohomematic/model/custom/const.py +2 -0
- aiohomematic/model/custom/cover.py +30 -2
- aiohomematic/model/custom/data_point.py +2 -0
- aiohomematic/model/custom/definition.py +2 -0
- aiohomematic/model/custom/light.py +18 -10
- aiohomematic/model/custom/lock.py +2 -0
- aiohomematic/model/custom/siren.py +5 -2
- aiohomematic/model/custom/support.py +2 -0
- aiohomematic/model/custom/switch.py +2 -0
- aiohomematic/model/custom/valve.py +2 -0
- aiohomematic/model/data_point.py +15 -3
- aiohomematic/model/decorators.py +29 -8
- aiohomematic/model/device.py +2 -0
- aiohomematic/model/event.py +2 -0
- aiohomematic/model/generic/__init__.py +2 -0
- aiohomematic/model/generic/action.py +2 -0
- aiohomematic/model/generic/binary_sensor.py +2 -0
- aiohomematic/model/generic/button.py +2 -0
- aiohomematic/model/generic/data_point.py +4 -1
- aiohomematic/model/generic/number.py +4 -1
- aiohomematic/model/generic/select.py +4 -1
- aiohomematic/model/generic/sensor.py +2 -0
- aiohomematic/model/generic/switch.py +2 -0
- aiohomematic/model/generic/text.py +2 -0
- aiohomematic/model/hub/__init__.py +2 -0
- aiohomematic/model/hub/binary_sensor.py +2 -0
- aiohomematic/model/hub/button.py +2 -0
- aiohomematic/model/hub/data_point.py +2 -0
- aiohomematic/model/hub/number.py +2 -0
- aiohomematic/model/hub/select.py +2 -0
- aiohomematic/model/hub/sensor.py +2 -0
- aiohomematic/model/hub/switch.py +2 -0
- aiohomematic/model/hub/text.py +2 -0
- aiohomematic/model/support.py +26 -1
- aiohomematic/model/update.py +2 -0
- aiohomematic/support.py +160 -3
- aiohomematic/validator.py +49 -2
- aiohomematic-2025.9.1.dist-info/METADATA +125 -0
- aiohomematic-2025.9.1.dist-info/RECORD +78 -0
- {aiohomematic-2025.8.9.dist-info → aiohomematic-2025.9.1.dist-info}/licenses/LICENSE +1 -1
- aiohomematic-2025.8.9.dist-info/METADATA +0 -69
- aiohomematic-2025.8.9.dist-info/RECORD +0 -77
- {aiohomematic-2025.8.9.dist-info → aiohomematic-2025.9.1.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.8.9.dist-info → aiohomematic-2025.9.1.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""Module for data points implemented using the light category."""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
@@ -471,29 +473,29 @@ class CustomDpIpRGBWLight(CustomDpDimmer):
|
|
|
471
473
|
@property
|
|
472
474
|
def _relevant_data_points(self) -> tuple[GenericDataPoint, ...]:
|
|
473
475
|
"""Returns the list of relevant data points. To be overridden by subclasses."""
|
|
474
|
-
if self.
|
|
476
|
+
if self._device_operation_mode == _DeviceOperationMode.RGBW:
|
|
475
477
|
return (
|
|
476
478
|
self._dp_hue,
|
|
477
479
|
self._dp_level,
|
|
478
480
|
self._dp_saturation,
|
|
479
481
|
self._dp_color_temperature_kelvin,
|
|
480
482
|
)
|
|
481
|
-
if self.
|
|
483
|
+
if self._device_operation_mode == _DeviceOperationMode.RGB:
|
|
482
484
|
return self._dp_hue, self._dp_level, self._dp_saturation
|
|
483
|
-
if self.
|
|
485
|
+
if self._device_operation_mode == _DeviceOperationMode.TUNABLE_WHITE:
|
|
484
486
|
return self._dp_level, self._dp_color_temperature_kelvin
|
|
485
487
|
return (self._dp_level,)
|
|
486
488
|
|
|
487
489
|
@property
|
|
488
490
|
def supports_color_temperature(self) -> bool:
|
|
489
491
|
"""Flag if light supports color temperature."""
|
|
490
|
-
return self.
|
|
492
|
+
return self._device_operation_mode == _DeviceOperationMode.TUNABLE_WHITE
|
|
491
493
|
|
|
492
494
|
@property
|
|
493
495
|
def supports_effects(self) -> bool:
|
|
494
496
|
"""Flag if light supports effects."""
|
|
495
497
|
return (
|
|
496
|
-
self.
|
|
498
|
+
self._device_operation_mode != _DeviceOperationMode.PWM
|
|
497
499
|
and self.effects is not None
|
|
498
500
|
and len(self.effects) > 0
|
|
499
501
|
)
|
|
@@ -501,7 +503,7 @@ class CustomDpIpRGBWLight(CustomDpDimmer):
|
|
|
501
503
|
@property
|
|
502
504
|
def supports_hs_color(self) -> bool:
|
|
503
505
|
"""Flag if light supports color."""
|
|
504
|
-
return self.
|
|
506
|
+
return self._device_operation_mode in (
|
|
505
507
|
_DeviceOperationMode.RGBW,
|
|
506
508
|
_DeviceOperationMode.RGB,
|
|
507
509
|
)
|
|
@@ -514,11 +516,9 @@ class CustomDpIpRGBWLight(CustomDpDimmer):
|
|
|
514
516
|
Avoid creating data points that are not usable in selected device operation mode.
|
|
515
517
|
"""
|
|
516
518
|
if (
|
|
517
|
-
self.
|
|
519
|
+
self._device_operation_mode in (_DeviceOperationMode.RGB, _DeviceOperationMode.RGBW)
|
|
518
520
|
and self._channel.no in (2, 3, 4)
|
|
519
|
-
) or (
|
|
520
|
-
self._dp_device_operation_mode.value == _DeviceOperationMode.TUNABLE_WHITE and self._channel.no in (3, 4)
|
|
521
|
-
):
|
|
521
|
+
) or (self._device_operation_mode == _DeviceOperationMode.TUNABLE_WHITE and self._channel.no in (3, 4)):
|
|
522
522
|
return DataPointUsage.NO_CREATE
|
|
523
523
|
return self._get_data_point_usage()
|
|
524
524
|
|
|
@@ -555,6 +555,13 @@ class CustomDpIpRGBWLight(CustomDpDimmer):
|
|
|
555
555
|
await self._set_on_time_value(on_time=_NOT_USED, collector=collector)
|
|
556
556
|
await super().turn_off(collector=collector, **kwargs)
|
|
557
557
|
|
|
558
|
+
@property
|
|
559
|
+
def _device_operation_mode(self) -> _DeviceOperationMode:
|
|
560
|
+
"""Return the device operation mode."""
|
|
561
|
+
if (mode := self._dp_device_operation_mode.value) is None:
|
|
562
|
+
return _DeviceOperationMode.RGBW
|
|
563
|
+
return _DeviceOperationMode(mode)
|
|
564
|
+
|
|
558
565
|
@bind_collector()
|
|
559
566
|
async def _set_on_time_value(self, on_time: float, collector: CallParameterCollector | None = None) -> None:
|
|
560
567
|
"""Set the on time value in seconds."""
|
|
@@ -1068,6 +1075,7 @@ DEVICES: Mapping[str, CustomConfig | tuple[CustomConfig, ...]] = {
|
|
|
1068
1075
|
"HmIP-FDT": CustomConfig(make_ce_func=make_ip_dimmer, channels=(2,)),
|
|
1069
1076
|
"HmIP-PDT": CustomConfig(make_ce_func=make_ip_dimmer, channels=(3,)),
|
|
1070
1077
|
"HmIP-RGBW": CustomConfig(make_ce_func=make_ip_rgbw_light),
|
|
1078
|
+
"HmIP-LSC": CustomConfig(make_ce_func=make_ip_rgbw_light),
|
|
1071
1079
|
"HmIP-SCTH230": CustomConfig(
|
|
1072
1080
|
make_ce_func=make_ip_dimmer,
|
|
1073
1081
|
channels=(12,),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""Module for data points implemented using the siren category."""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
@@ -8,6 +10,7 @@ from enum import StrEnum
|
|
|
8
10
|
from typing import Final, TypedDict, Unpack
|
|
9
11
|
|
|
10
12
|
from aiohomematic.const import DataPointCategory
|
|
13
|
+
from aiohomematic.exceptions import ValidationException
|
|
11
14
|
from aiohomematic.model import device as hmd
|
|
12
15
|
from aiohomematic.model.custom import definition as hmed
|
|
13
16
|
from aiohomematic.model.custom.const import DeviceProfile, Field
|
|
@@ -147,14 +150,14 @@ class CustomDpIpSiren(BaseCustomDpSiren):
|
|
|
147
150
|
|
|
148
151
|
acoustic_alarm = kwargs.get("acoustic_alarm", self._dp_acoustic_alarm_selection.default)
|
|
149
152
|
if self.available_tones and acoustic_alarm and acoustic_alarm not in self.available_tones:
|
|
150
|
-
raise
|
|
153
|
+
raise ValidationException(
|
|
151
154
|
f"Invalid tone specified for data_point {self.full_name}: {acoustic_alarm}, "
|
|
152
155
|
"check the available_tones attribute for valid tones to pass in"
|
|
153
156
|
)
|
|
154
157
|
|
|
155
158
|
optical_alarm = kwargs.get("optical_alarm", self._dp_optical_alarm_selection.default)
|
|
156
159
|
if self.available_lights and optical_alarm and optical_alarm not in self.available_lights:
|
|
157
|
-
raise
|
|
160
|
+
raise ValidationException(
|
|
158
161
|
f"Invalid light specified for data_point {self.full_name}: {optical_alarm}, "
|
|
159
162
|
"check the available_lights attribute for valid tones to pass in"
|
|
160
163
|
)
|
aiohomematic/model/data_point.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""
|
|
2
4
|
Core data point model for AioHomematic.
|
|
3
5
|
|
|
@@ -80,7 +82,6 @@ __all__ = [
|
|
|
80
82
|
"BaseParameterDataPoint",
|
|
81
83
|
"CallParameterCollector",
|
|
82
84
|
"CallbackDataPoint",
|
|
83
|
-
"EVENT_DATA_SCHEMA",
|
|
84
85
|
"bind_collector",
|
|
85
86
|
]
|
|
86
87
|
|
|
@@ -1085,12 +1086,23 @@ def bind_collector(
|
|
|
1085
1086
|
IN_SERVICE_VAR.reset(token)
|
|
1086
1087
|
in_service = IN_SERVICE_VAR.get()
|
|
1087
1088
|
if not in_service and log_level > logging.NOTSET:
|
|
1088
|
-
logging.getLogger(args[0].__module__)
|
|
1089
|
+
logger = logging.getLogger(args[0].__module__)
|
|
1090
|
+
extra = {
|
|
1091
|
+
"err_type": bhexc.__class__.__name__,
|
|
1092
|
+
"err": extract_exc_args(exc=bhexc),
|
|
1093
|
+
"function": func.__name__,
|
|
1094
|
+
**hms.build_log_context_from_obj(obj=args[0]),
|
|
1095
|
+
}
|
|
1096
|
+
if log_level >= logging.ERROR:
|
|
1097
|
+
logger.exception("service_error", extra=extra)
|
|
1098
|
+
else:
|
|
1099
|
+
logger.log(level=log_level, msg="service_error", extra=extra)
|
|
1100
|
+
# Re-raise domain-specific exceptions so callers and tests can handle them
|
|
1101
|
+
raise
|
|
1089
1102
|
else:
|
|
1090
1103
|
if token:
|
|
1091
1104
|
IN_SERVICE_VAR.reset(token)
|
|
1092
1105
|
return return_value
|
|
1093
|
-
return None
|
|
1094
1106
|
|
|
1095
1107
|
setattr(bind_wrapper, "ha_service", True)
|
|
1096
1108
|
return bind_wrapper # type: ignore[return-value]
|
aiohomematic/model/decorators.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""Decorators for data points used within aiohomematic."""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
@@ -6,6 +8,7 @@ from collections.abc import Callable, Mapping
|
|
|
6
8
|
from datetime import datetime
|
|
7
9
|
from enum import Enum
|
|
8
10
|
from typing import Any, ParamSpec, TypeVar, cast
|
|
11
|
+
from weakref import WeakKeyDictionary
|
|
9
12
|
|
|
10
13
|
__all__ = [
|
|
11
14
|
"config_property",
|
|
@@ -18,7 +21,6 @@ __all__ = [
|
|
|
18
21
|
|
|
19
22
|
P = ParamSpec("P")
|
|
20
23
|
T = TypeVar("T")
|
|
21
|
-
R = TypeVar("R")
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
# pylint: disable=invalid-name
|
|
@@ -91,8 +93,9 @@ class state_property[GETTER, SETTER](generic_property[GETTER, SETTER]):
|
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
# Cache for per-class attribute names by decorator to avoid repeated dir() scans
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
+
# Use WeakKeyDictionary to allow classes to be garbage-collected without leaking cache entries.
|
|
97
|
+
# Structure: {cls: {decorator_class: (attr_name1, attr_name2, ...)}}
|
|
98
|
+
_PUBLIC_ATTR_CACHE: WeakKeyDictionary[type, dict[type, tuple[str, ...]]] = WeakKeyDictionary()
|
|
96
99
|
|
|
97
100
|
|
|
98
101
|
def _get_public_attributes_by_class_decorator(data_object: Any, class_decorator: type) -> Mapping[str, Any]:
|
|
@@ -102,15 +105,33 @@ def _get_public_attributes_by_class_decorator(data_object: Any, class_decorator:
|
|
|
102
105
|
This caches the attribute names per (class, decorator) to reduce overhead
|
|
103
106
|
from repeated dir()/getattr() scans. Values are not cached as they are
|
|
104
107
|
instance-dependent and may change over time.
|
|
108
|
+
|
|
109
|
+
To minimize side effects, exceptions raised by property getters are caught
|
|
110
|
+
and the corresponding value is set to None. This ensures that payload
|
|
111
|
+
construction and attribute introspection do not fail due to individual
|
|
112
|
+
properties with transient errors or expensive side effects.
|
|
105
113
|
"""
|
|
106
114
|
cls = data_object.__class__
|
|
107
|
-
cache_key = (cls, class_decorator)
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
# Get or create the per-class cache dict
|
|
117
|
+
if (decorator_cache := _PUBLIC_ATTR_CACHE.get(cls)) is None:
|
|
118
|
+
decorator_cache = {}
|
|
119
|
+
_PUBLIC_ATTR_CACHE[cls] = decorator_cache
|
|
112
120
|
|
|
113
|
-
|
|
121
|
+
# Get or compute the attribute names for this decorator
|
|
122
|
+
if (names := decorator_cache.get(class_decorator)) is None:
|
|
123
|
+
names = tuple(y for y in dir(cls) if not y.startswith("_") and isinstance(getattr(cls, y), class_decorator))
|
|
124
|
+
decorator_cache[class_decorator] = names
|
|
125
|
+
|
|
126
|
+
result: dict[str, Any] = {}
|
|
127
|
+
for name in names:
|
|
128
|
+
try:
|
|
129
|
+
value = getattr(data_object, name)
|
|
130
|
+
except Exception:
|
|
131
|
+
# Avoid propagating side effects/errors from getters
|
|
132
|
+
value = None
|
|
133
|
+
result[name] = _get_text_value(value)
|
|
134
|
+
return result
|
|
114
135
|
|
|
115
136
|
|
|
116
137
|
def _get_text_value(value: Any) -> Any:
|
aiohomematic/model/device.py
CHANGED
aiohomematic/model/event.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""Generic python representation of a CCU parameter."""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
@@ -16,6 +18,7 @@ from aiohomematic.const import (
|
|
|
16
18
|
ParamsetKey,
|
|
17
19
|
)
|
|
18
20
|
from aiohomematic.decorators import inspector
|
|
21
|
+
from aiohomematic.exceptions import ValidationException
|
|
19
22
|
from aiohomematic.model import data_point as hme, device as hmd
|
|
20
23
|
from aiohomematic.model.decorators import cached_slot_property
|
|
21
24
|
from aiohomematic.model.support import DataPointNameData, GenericParameterType, get_data_point_name_data
|
|
@@ -101,7 +104,7 @@ class GenericDataPoint[ParameterT: GenericParameterType, InputParameterT: Generi
|
|
|
101
104
|
return set()
|
|
102
105
|
try:
|
|
103
106
|
prepared_value = self._prepare_value_for_sending(value=value, do_validate=do_validate)
|
|
104
|
-
except ValueError as verr:
|
|
107
|
+
except (ValueError, ValidationException) as verr:
|
|
105
108
|
_LOGGER.warning(verr)
|
|
106
109
|
return set()
|
|
107
110
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""Module for data points implemented using the number category."""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
@@ -5,6 +7,7 @@ from __future__ import annotations
|
|
|
5
7
|
from typing import cast
|
|
6
8
|
|
|
7
9
|
from aiohomematic.const import DataPointCategory
|
|
10
|
+
from aiohomematic.exceptions import ValidationException
|
|
8
11
|
from aiohomematic.model.decorators import state_property
|
|
9
12
|
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
10
13
|
|
|
@@ -30,7 +33,7 @@ class BaseDpNumber[NumberParameterT: int | float | None](GenericDataPoint[Number
|
|
|
30
33
|
return cast(NumberParameterT, type_converter(value))
|
|
31
34
|
if self._special and isinstance(value, str) and value in self._special:
|
|
32
35
|
return cast(NumberParameterT, type_converter(self._special[value]))
|
|
33
|
-
raise
|
|
36
|
+
raise ValidationException(
|
|
34
37
|
f"NUMBER failed: Invalid value: {value} (min: {self._min}, max: {self._max}, special:{self._special})"
|
|
35
38
|
)
|
|
36
39
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""Module for data points implemented using the select category."""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
4
6
|
|
|
5
7
|
from aiohomematic.const import DataPointCategory
|
|
8
|
+
from aiohomematic.exceptions import ValidationException
|
|
6
9
|
from aiohomematic.model.decorators import state_property
|
|
7
10
|
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
8
11
|
from aiohomematic.model.support import get_value_from_value_list
|
|
@@ -33,4 +36,4 @@ class DpSelect(GenericDataPoint[int | str, int | float | str]):
|
|
|
33
36
|
return int(value)
|
|
34
37
|
if self._values and value in self._values:
|
|
35
38
|
return self._values.index(value)
|
|
36
|
-
raise
|
|
39
|
+
raise ValidationException(f"Value not in value_list for {self.name}/{self.unique_id}")
|
aiohomematic/model/hub/button.py
CHANGED
aiohomematic/model/hub/number.py
CHANGED
aiohomematic/model/hub/select.py
CHANGED
aiohomematic/model/hub/sensor.py
CHANGED
aiohomematic/model/hub/switch.py
CHANGED
aiohomematic/model/hub/text.py
CHANGED
aiohomematic/model/support.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""Support for data points used within aiohomematic."""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
@@ -5,6 +7,7 @@ from __future__ import annotations
|
|
|
5
7
|
from abc import abstractmethod
|
|
6
8
|
from collections.abc import Mapping
|
|
7
9
|
from enum import StrEnum
|
|
10
|
+
from functools import lru_cache
|
|
8
11
|
import logging
|
|
9
12
|
from typing import Any, Final
|
|
10
13
|
|
|
@@ -533,7 +536,29 @@ def _check_channel_name_with_channel_no(name: str) -> bool:
|
|
|
533
536
|
|
|
534
537
|
|
|
535
538
|
def convert_value(value: Any, target_type: ParameterType, value_list: tuple[str, ...] | None) -> Any:
|
|
536
|
-
"""
|
|
539
|
+
"""
|
|
540
|
+
Convert a value to target_type with safe memoization.
|
|
541
|
+
|
|
542
|
+
To avoid redundant conversions across layers, we use an internal
|
|
543
|
+
LRU-cached helper for hashable inputs. For unhashable inputs, we
|
|
544
|
+
fall back to a direct conversion path.
|
|
545
|
+
"""
|
|
546
|
+
# Normalize value_list to tuple to ensure hashability where possible
|
|
547
|
+
norm_value_list: tuple[str, ...] | None = tuple(value_list) if isinstance(value_list, list) else value_list
|
|
548
|
+
try:
|
|
549
|
+
# This will be cached if all arguments are hashable
|
|
550
|
+
return _convert_value_cached(value, target_type, norm_value_list)
|
|
551
|
+
except TypeError:
|
|
552
|
+
# Fallback non-cached path if any argument is unhashable
|
|
553
|
+
return _convert_value_noncached(value, target_type, norm_value_list)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@lru_cache(maxsize=2048)
|
|
557
|
+
def _convert_value_cached(value: Any, target_type: ParameterType, value_list: tuple[str, ...] | None) -> Any:
|
|
558
|
+
return _convert_value_noncached(value, target_type, value_list)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _convert_value_noncached(value: Any, target_type: ParameterType, value_list: tuple[str, ...] | None) -> Any:
|
|
537
562
|
if value is None:
|
|
538
563
|
return None
|
|
539
564
|
if target_type == ParameterType.BOOL:
|