PyPlumIO 0.5.42__py3-none-any.whl → 0.5.44__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.
- pyplumio/__init__.py +3 -2
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +14 -14
- pyplumio/const.py +8 -3
- pyplumio/{helpers/data_types.py → data_types.py} +23 -21
- pyplumio/devices/__init__.py +42 -41
- pyplumio/devices/ecomax.py +202 -174
- pyplumio/devices/ecoster.py +5 -0
- pyplumio/devices/mixer.py +24 -34
- pyplumio/devices/thermostat.py +24 -31
- pyplumio/filters.py +188 -147
- pyplumio/frames/__init__.py +20 -8
- pyplumio/frames/messages.py +3 -0
- pyplumio/frames/requests.py +21 -0
- pyplumio/frames/responses.py +18 -0
- pyplumio/helpers/async_cache.py +48 -0
- pyplumio/helpers/event_manager.py +58 -3
- pyplumio/helpers/factory.py +5 -2
- pyplumio/helpers/schedule.py +8 -5
- pyplumio/helpers/task_manager.py +3 -0
- pyplumio/helpers/timeout.py +7 -6
- pyplumio/helpers/uid.py +8 -5
- pyplumio/{helpers/parameter.py → parameters/__init__.py} +105 -5
- pyplumio/parameters/ecomax.py +868 -0
- pyplumio/parameters/mixer.py +245 -0
- pyplumio/parameters/thermostat.py +197 -0
- pyplumio/protocol.py +21 -10
- pyplumio/stream.py +3 -0
- pyplumio/structures/__init__.py +3 -0
- pyplumio/structures/alerts.py +9 -6
- pyplumio/structures/boiler_load.py +3 -0
- pyplumio/structures/boiler_power.py +4 -1
- pyplumio/structures/ecomax_parameters.py +6 -800
- pyplumio/structures/fan_power.py +4 -1
- pyplumio/structures/frame_versions.py +4 -1
- pyplumio/structures/fuel_consumption.py +4 -1
- pyplumio/structures/fuel_level.py +3 -0
- pyplumio/structures/lambda_sensor.py +9 -1
- pyplumio/structures/mixer_parameters.py +8 -230
- pyplumio/structures/mixer_sensors.py +10 -1
- pyplumio/structures/modules.py +14 -0
- pyplumio/structures/network_info.py +12 -1
- pyplumio/structures/output_flags.py +10 -1
- pyplumio/structures/outputs.py +22 -1
- pyplumio/structures/pending_alerts.py +3 -0
- pyplumio/structures/product_info.py +6 -3
- pyplumio/structures/program_version.py +3 -0
- pyplumio/structures/regulator_data.py +5 -2
- pyplumio/structures/regulator_data_schema.py +4 -1
- pyplumio/structures/schedules.py +18 -1
- pyplumio/structures/statuses.py +9 -0
- pyplumio/structures/temperatures.py +23 -1
- pyplumio/structures/thermostat_parameters.py +18 -184
- pyplumio/structures/thermostat_sensors.py +10 -1
- pyplumio/utils.py +14 -12
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/METADATA +32 -17
- pyplumio-0.5.44.dist-info/RECORD +64 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/WHEEL +1 -1
- pyplumio-0.5.42.dist-info/RECORD +0 -60
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.44.dist-info}/top_level.txt +0 -0
pyplumio/frames/responses.py
CHANGED
@@ -221,3 +221,21 @@ class UIDResponse(Response):
|
|
221
221
|
def decode_message(self, message: bytearray) -> dict[str, Any]:
|
222
222
|
"""Decode a frame message."""
|
223
223
|
return ProductInfoStructure(self).decode(message)[0]
|
224
|
+
|
225
|
+
|
226
|
+
__all__ = [
|
227
|
+
"AlertsResponse",
|
228
|
+
"DeviceAvailableResponse",
|
229
|
+
"EcomaxControlResponse",
|
230
|
+
"EcomaxParametersResponse",
|
231
|
+
"MixerParametersResponse",
|
232
|
+
"PasswordResponse",
|
233
|
+
"ProgramVersionResponse",
|
234
|
+
"RegulatorDataSchemaResponse",
|
235
|
+
"SchedulesResponse",
|
236
|
+
"SetEcomaxParameterResponse",
|
237
|
+
"SetMixerParameterResponse",
|
238
|
+
"SetThermostatParameterResponse",
|
239
|
+
"ThermostatParametersResponse",
|
240
|
+
"UIDResponse",
|
241
|
+
]
|
@@ -0,0 +1,48 @@
|
|
1
|
+
"""Contains a simple async cache for caching results of async functions."""
|
2
|
+
|
3
|
+
from collections.abc import Awaitable
|
4
|
+
from functools import wraps
|
5
|
+
from typing import Any, Callable, TypeVar, cast
|
6
|
+
|
7
|
+
from typing_extensions import ParamSpec
|
8
|
+
|
9
|
+
T = TypeVar("T")
|
10
|
+
P = ParamSpec("P")
|
11
|
+
|
12
|
+
|
13
|
+
class AsyncCache:
|
14
|
+
"""A simple cache for asynchronous functions."""
|
15
|
+
|
16
|
+
__slots__ = ("cache",)
|
17
|
+
|
18
|
+
cache: dict[str, Any]
|
19
|
+
|
20
|
+
def __init__(self) -> None:
|
21
|
+
"""Initialize the cache."""
|
22
|
+
self.cache = {}
|
23
|
+
|
24
|
+
async def get(self, key: str, coro: Callable[..., Awaitable[Any]]) -> Any:
|
25
|
+
"""Get a value from the cache or compute and store it."""
|
26
|
+
if key not in self.cache:
|
27
|
+
self.cache[key] = await coro()
|
28
|
+
|
29
|
+
return self.cache[key]
|
30
|
+
|
31
|
+
|
32
|
+
# Create a global cache instance
|
33
|
+
async_cache = AsyncCache()
|
34
|
+
|
35
|
+
|
36
|
+
def acache(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
37
|
+
"""Cache the result of an async function."""
|
38
|
+
|
39
|
+
@wraps(func)
|
40
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
41
|
+
func_name = f"{func.__module__}.{func.__qualname__}"
|
42
|
+
key = f"{func_name}:{args}:{kwargs}"
|
43
|
+
return cast(T, await async_cache.get(key, lambda: func(*args, **kwargs)))
|
44
|
+
|
45
|
+
return wrapper
|
46
|
+
|
47
|
+
|
48
|
+
__all__ = ["acache", "async_cache"]
|
@@ -3,7 +3,8 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Callable, Coroutine
|
6
|
+
from collections.abc import Callable, Coroutine, Generator
|
7
|
+
import inspect
|
7
8
|
from typing import Any, Generic, TypeVar, overload
|
8
9
|
|
9
10
|
from typing_extensions import TypeAlias
|
@@ -11,7 +12,45 @@ from typing_extensions import TypeAlias
|
|
11
12
|
from pyplumio.helpers.task_manager import TaskManager
|
12
13
|
|
13
14
|
Callback: TypeAlias = Callable[[Any], Coroutine[Any, Any, Any]]
|
14
|
-
|
15
|
+
|
16
|
+
_CallableT: TypeAlias = Callable[..., Any]
|
17
|
+
_CallbackT = TypeVar("_CallbackT", bound=Callback)
|
18
|
+
|
19
|
+
|
20
|
+
@overload
|
21
|
+
def event_listener(name: _CallableT, filter: None = None) -> Callback: ...
|
22
|
+
|
23
|
+
|
24
|
+
@overload
|
25
|
+
def event_listener(
|
26
|
+
name: str | None = None, filter: _CallableT | None = None
|
27
|
+
) -> _CallableT: ...
|
28
|
+
|
29
|
+
|
30
|
+
def event_listener(name: Any = None, filter: _CallableT | None = None) -> Any:
|
31
|
+
"""Mark a function as an event listener.
|
32
|
+
|
33
|
+
This decorator attaches metadata to the function, identifying it
|
34
|
+
as a subscriber for the specified event.
|
35
|
+
"""
|
36
|
+
|
37
|
+
def decorator(func: _CallbackT) -> _CallbackT:
|
38
|
+
# Attach metadata to the function to mark it as a listener.
|
39
|
+
event = (
|
40
|
+
name
|
41
|
+
if isinstance(name, str)
|
42
|
+
else func.__qualname__.split("on_event_", 1)[1]
|
43
|
+
)
|
44
|
+
setattr(func, "_on_event", event)
|
45
|
+
setattr(func, "_on_event_filter", filter)
|
46
|
+
return func
|
47
|
+
|
48
|
+
if callable(name):
|
49
|
+
return decorator(name)
|
50
|
+
else:
|
51
|
+
return decorator
|
52
|
+
|
53
|
+
|
15
54
|
T = TypeVar("T")
|
16
55
|
|
17
56
|
|
@@ -30,6 +69,7 @@ class EventManager(TaskManager, Generic[T]):
|
|
30
69
|
self.data = {}
|
31
70
|
self._events = {}
|
32
71
|
self._callbacks = {}
|
72
|
+
self._register_event_listeners()
|
33
73
|
|
34
74
|
def __getattr__(self, name: str) -> T:
|
35
75
|
"""Return attributes from the underlying data dictionary."""
|
@@ -38,6 +78,18 @@ class EventManager(TaskManager, Generic[T]):
|
|
38
78
|
except KeyError as e:
|
39
79
|
raise AttributeError from e
|
40
80
|
|
81
|
+
def _register_event_listeners(self) -> None:
|
82
|
+
"""Register the event listeners."""
|
83
|
+
for event, callback in self.event_listeners():
|
84
|
+
filter_func = getattr(callback, "_on_event_filter", None)
|
85
|
+
self.subscribe(event, filter_func(callback) if filter_func else callback)
|
86
|
+
|
87
|
+
def event_listeners(self) -> Generator[tuple[str, Callback]]:
|
88
|
+
"""Get the event listeners."""
|
89
|
+
for _, callback in inspect.getmembers(self, predicate=inspect.ismethod):
|
90
|
+
if event := getattr(callback, "_on_event", None):
|
91
|
+
yield (event, callback)
|
92
|
+
|
41
93
|
async def wait_for(self, name: str, timeout: float | None = None) -> None:
|
42
94
|
"""Wait for the value to become available.
|
43
95
|
|
@@ -91,7 +143,7 @@ class EventManager(TaskManager, Generic[T]):
|
|
91
143
|
except KeyError:
|
92
144
|
return default
|
93
145
|
|
94
|
-
def subscribe(self, name: str, callback:
|
146
|
+
def subscribe(self, name: str, callback: _CallbackT) -> _CallbackT:
|
95
147
|
"""Subscribe a callback to the event.
|
96
148
|
|
97
149
|
:param name: Event name or ID
|
@@ -189,3 +241,6 @@ class EventManager(TaskManager, Generic[T]):
|
|
189
241
|
def events(self) -> dict[str, asyncio.Event]:
|
190
242
|
"""Return the events."""
|
191
243
|
return self._events
|
244
|
+
|
245
|
+
|
246
|
+
__all__ = ["Callback", "EventManager", "event_listener"]
|
pyplumio/helpers/factory.py
CHANGED
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
13
13
|
T = TypeVar("T")
|
14
14
|
|
15
15
|
|
16
|
-
async def
|
16
|
+
async def import_module(name: str) -> ModuleType:
|
17
17
|
"""Import module by name."""
|
18
18
|
loop = asyncio.get_running_loop()
|
19
19
|
return await loop.run_in_executor(None, importlib.import_module, f"pyplumio.{name}")
|
@@ -23,7 +23,7 @@ async def create_instance(class_path: str, /, cls: type[T], **kwargs: Any) -> T:
|
|
23
23
|
"""Return a class instance from the class path."""
|
24
24
|
module_name, class_name = class_path.rsplit(".", 1)
|
25
25
|
try:
|
26
|
-
module = await
|
26
|
+
module = await import_module(module_name)
|
27
27
|
instance = getattr(module, class_name)(**kwargs)
|
28
28
|
if not isinstance(instance, cls):
|
29
29
|
raise TypeError(
|
@@ -35,3 +35,6 @@ async def create_instance(class_path: str, /, cls: type[T], **kwargs: Any) -> T:
|
|
35
35
|
except Exception:
|
36
36
|
_LOGGER.exception("Failed to create instance for class path '%s'", class_path)
|
37
37
|
raise
|
38
|
+
|
39
|
+
|
40
|
+
__all__ = ["create_instance"]
|
pyplumio/helpers/schedule.py
CHANGED
@@ -23,7 +23,7 @@ STEP = dt.timedelta(minutes=30)
|
|
23
23
|
Time = Annotated[str, "Time string in %H:%M format"]
|
24
24
|
|
25
25
|
|
26
|
-
def
|
26
|
+
def get_time(
|
27
27
|
index: int, start: dt.datetime = MIDNIGHT_DT, step: dt.timedelta = STEP
|
28
28
|
) -> Time:
|
29
29
|
"""Return time for a specific index."""
|
@@ -32,7 +32,7 @@ def _get_time(
|
|
32
32
|
|
33
33
|
|
34
34
|
@lru_cache(maxsize=10)
|
35
|
-
def
|
35
|
+
def get_time_range(start: Time, end: Time, step: dt.timedelta = STEP) -> list[Time]:
|
36
36
|
"""Get a time range.
|
37
37
|
|
38
38
|
Start and end boundaries should be specified in %H:%M format.
|
@@ -54,7 +54,7 @@ def _get_time_range(start: Time, end: Time, step: dt.timedelta = STEP) -> list[T
|
|
54
54
|
seconds = (end_dt - start_dt).total_seconds()
|
55
55
|
steps = seconds // step.total_seconds() + 1
|
56
56
|
|
57
|
-
return [
|
57
|
+
return [get_time(index, start=start_dt, step=step) for index in range(int(steps))]
|
58
58
|
|
59
59
|
|
60
60
|
class ScheduleDay(MutableMapping):
|
@@ -104,7 +104,7 @@ class ScheduleDay(MutableMapping):
|
|
104
104
|
self, state: State | bool, start: Time = MIDNIGHT, end: Time = MIDNIGHT
|
105
105
|
) -> None:
|
106
106
|
"""Set a schedule interval state."""
|
107
|
-
for time in
|
107
|
+
for time in get_time_range(start, end):
|
108
108
|
self.__setitem__(time, state)
|
109
109
|
|
110
110
|
def set_on(self, start: Time = MIDNIGHT, end: Time = MIDNIGHT) -> None:
|
@@ -123,7 +123,7 @@ class ScheduleDay(MutableMapping):
|
|
123
123
|
@classmethod
|
124
124
|
def from_iterable(cls: type[ScheduleDay], intervals: Iterable[bool]) -> ScheduleDay:
|
125
125
|
"""Make schedule day from iterable."""
|
126
|
-
return cls({
|
126
|
+
return cls({get_time(index): state for index, state in enumerate(intervals)})
|
127
127
|
|
128
128
|
|
129
129
|
@dataclass
|
@@ -174,3 +174,6 @@ class Schedule(Iterable):
|
|
174
174
|
data=collect_schedule_data(self.name, self.device),
|
175
175
|
)
|
176
176
|
)
|
177
|
+
|
178
|
+
|
179
|
+
__all__ = ["Schedule", "ScheduleDay"]
|
pyplumio/helpers/task_manager.py
CHANGED
pyplumio/helpers/timeout.py
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Awaitable, Callable
|
6
|
+
from collections.abc import Awaitable, Callable
|
7
7
|
from functools import wraps
|
8
|
-
from typing import
|
8
|
+
from typing import TypeVar
|
9
9
|
|
10
10
|
from typing_extensions import ParamSpec
|
11
11
|
|
@@ -15,12 +15,10 @@ P = ParamSpec("P")
|
|
15
15
|
|
16
16
|
def timeout(
|
17
17
|
seconds: float,
|
18
|
-
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P,
|
18
|
+
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
|
19
19
|
"""Decorate a timeout for the awaitable."""
|
20
20
|
|
21
|
-
def decorator(
|
22
|
-
func: Callable[P, Awaitable[T]],
|
23
|
-
) -> Callable[P, Coroutine[Any, Any, T]]:
|
21
|
+
def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
24
22
|
@wraps(func)
|
25
23
|
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
26
24
|
return await asyncio.wait_for(func(*args, **kwargs), timeout=seconds)
|
@@ -28,3 +26,6 @@ def timeout(
|
|
28
26
|
return wrapper
|
29
27
|
|
30
28
|
return decorator
|
29
|
+
|
30
|
+
|
31
|
+
__all__ = ["timeout"]
|
pyplumio/helpers/uid.py
CHANGED
@@ -12,10 +12,10 @@ BASE5_KEY: Final = "0123456789ABCDEFGHIJKLMNZPQRSTUV"
|
|
12
12
|
|
13
13
|
def unpack_uid(buffer: bytes) -> str:
|
14
14
|
"""Unpack UID from bytes."""
|
15
|
-
return
|
15
|
+
return base5(buffer + crc16(buffer))
|
16
16
|
|
17
17
|
|
18
|
-
def
|
18
|
+
def base5(buffer: bytes) -> str:
|
19
19
|
"""Encode bytes to a base5 encoded string."""
|
20
20
|
number = int.from_bytes(buffer, byteorder="little")
|
21
21
|
output = []
|
@@ -26,16 +26,19 @@ def _base5(buffer: bytes) -> str:
|
|
26
26
|
return "".join(reversed(output))
|
27
27
|
|
28
28
|
|
29
|
-
def
|
29
|
+
def crc16(buffer: bytes) -> bytes:
|
30
30
|
"""Return a CRC 16."""
|
31
|
-
crc16 = reduce(
|
31
|
+
crc16 = reduce(crc16_byte, buffer, CRC)
|
32
32
|
return crc16.to_bytes(length=2, byteorder="little")
|
33
33
|
|
34
34
|
|
35
|
-
def
|
35
|
+
def crc16_byte(crc: int, byte: int) -> int:
|
36
36
|
"""Add a byte to the CRC."""
|
37
37
|
crc ^= byte
|
38
38
|
for _ in range(8):
|
39
39
|
crc = (crc >> 1) ^ POLYNOMIAL if crc & 1 else crc >> 1
|
40
40
|
|
41
41
|
return crc
|
42
|
+
|
43
|
+
|
44
|
+
__all__ = ["unpack_uid"]
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
import asyncio
|
7
|
+
from collections.abc import Sequence
|
7
8
|
from dataclasses import dataclass
|
8
9
|
import logging
|
9
10
|
from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
@@ -11,8 +12,16 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
|
11
12
|
from dataslots import dataslots
|
12
13
|
from typing_extensions import TypeAlias
|
13
14
|
|
14
|
-
from pyplumio.const import
|
15
|
+
from pyplumio.const import (
|
16
|
+
BYTE_UNDEFINED,
|
17
|
+
STATE_OFF,
|
18
|
+
STATE_ON,
|
19
|
+
ProductModel,
|
20
|
+
State,
|
21
|
+
UnitOfMeasurement,
|
22
|
+
)
|
15
23
|
from pyplumio.frames import Request
|
24
|
+
from pyplumio.structures.product_info import ProductInfo
|
16
25
|
from pyplumio.utils import is_divisible
|
17
26
|
|
18
27
|
if TYPE_CHECKING:
|
@@ -20,8 +29,6 @@ if TYPE_CHECKING:
|
|
20
29
|
|
21
30
|
_LOGGER = logging.getLogger(__name__)
|
22
31
|
|
23
|
-
|
24
|
-
NumericType: TypeAlias = Union[int, float]
|
25
32
|
ParameterT = TypeVar("ParameterT", bound="Parameter")
|
26
33
|
|
27
34
|
|
@@ -68,6 +75,9 @@ class ParameterDescription:
|
|
68
75
|
optimistic: bool = False
|
69
76
|
|
70
77
|
|
78
|
+
NumericType: TypeAlias = Union[int, float]
|
79
|
+
|
80
|
+
|
71
81
|
class Parameter(ABC):
|
72
82
|
"""Represents a base parameter."""
|
73
83
|
|
@@ -186,7 +196,7 @@ class Parameter(ABC):
|
|
186
196
|
self, value: int, retries: int = 5, timeout: float = 5.0
|
187
197
|
) -> bool:
|
188
198
|
"""Attempt to update a parameter value on the remote device."""
|
189
|
-
_LOGGER.
|
199
|
+
_LOGGER.info(
|
190
200
|
"Attempting to update '%s' parameter to %d", self.description.name, value
|
191
201
|
)
|
192
202
|
if value == self.values.value:
|
@@ -360,6 +370,30 @@ class Number(Parameter):
|
|
360
370
|
return self.description.unit_of_measurement
|
361
371
|
|
362
372
|
|
373
|
+
@dataslots
|
374
|
+
@dataclass
|
375
|
+
class OffsetNumberDescription(NumberDescription):
|
376
|
+
"""Represents a parameter description."""
|
377
|
+
|
378
|
+
offset: int = 0
|
379
|
+
|
380
|
+
|
381
|
+
class OffsetNumber(Number):
|
382
|
+
"""Represents a number with offset."""
|
383
|
+
|
384
|
+
__slots__ = ()
|
385
|
+
|
386
|
+
description: OffsetNumberDescription
|
387
|
+
|
388
|
+
def _pack_value(self, value: NumericType) -> int:
|
389
|
+
"""Pack the parameter value."""
|
390
|
+
return super()._pack_value(value + self.description.offset)
|
391
|
+
|
392
|
+
def _unpack_value(self, value: int) -> NumericType:
|
393
|
+
"""Unpack the parameter value."""
|
394
|
+
return super()._unpack_value(value - self.description.offset)
|
395
|
+
|
396
|
+
|
363
397
|
@dataslots
|
364
398
|
@dataclass
|
365
399
|
class SwitchDescription(ParameterDescription):
|
@@ -387,7 +421,10 @@ class Switch(Parameter):
|
|
387
421
|
def validate(self, value: Any) -> bool:
|
388
422
|
"""Validate a parameter value."""
|
389
423
|
if not isinstance(value, bool) and value not in get_args(State):
|
390
|
-
raise ValueError(
|
424
|
+
raise ValueError(
|
425
|
+
f"Invalid value: {value}. The value must be either 'on', 'off' or "
|
426
|
+
f"boolean."
|
427
|
+
)
|
391
428
|
|
392
429
|
return True
|
393
430
|
|
@@ -447,3 +484,66 @@ class Switch(Parameter):
|
|
447
484
|
def max_value(self) -> Literal["on"]:
|
448
485
|
"""Return the maximum allowed value."""
|
449
486
|
return STATE_ON
|
487
|
+
|
488
|
+
|
489
|
+
@dataclass
|
490
|
+
class ParameterOverride:
|
491
|
+
"""Represents a parameter override."""
|
492
|
+
|
493
|
+
__slot__ = ("original", "replacement", "product_model", "product_id")
|
494
|
+
|
495
|
+
original: str
|
496
|
+
replacement: ParameterDescription
|
497
|
+
product_model: ProductModel
|
498
|
+
product_id: int
|
499
|
+
|
500
|
+
|
501
|
+
_DescriptorT = TypeVar("_DescriptorT", bound=ParameterDescription)
|
502
|
+
|
503
|
+
|
504
|
+
def patch_parameter_types(
|
505
|
+
product_info: ProductInfo,
|
506
|
+
parameter_types: list[_DescriptorT],
|
507
|
+
parameter_overrides: Sequence[ParameterOverride],
|
508
|
+
) -> list[_DescriptorT]:
|
509
|
+
"""Patch the parameter types based on the provided overrides.
|
510
|
+
|
511
|
+
Note:
|
512
|
+
The `# type: ignore[assignment]` comment is used to suppress a
|
513
|
+
type-checking error caused by mypy bug. For more details, see:
|
514
|
+
https://github.com/python/mypy/issues/13596
|
515
|
+
|
516
|
+
"""
|
517
|
+
replacements = {
|
518
|
+
override.original: override.replacement
|
519
|
+
for override in parameter_overrides
|
520
|
+
if override.product_model.value == product_info.model
|
521
|
+
and override.product_id == product_info.id
|
522
|
+
}
|
523
|
+
for index, description in enumerate(parameter_types):
|
524
|
+
if description.name in replacements:
|
525
|
+
_LOGGER.info(
|
526
|
+
"Replacing parameter description for '%s' with '%s' (%s)",
|
527
|
+
description.name,
|
528
|
+
replacements[description.name],
|
529
|
+
product_info.model,
|
530
|
+
)
|
531
|
+
parameter_types[index] = replacements[description.name] # type: ignore[assignment]
|
532
|
+
|
533
|
+
return parameter_types
|
534
|
+
|
535
|
+
|
536
|
+
__all__ = [
|
537
|
+
"Number",
|
538
|
+
"NumberDescription",
|
539
|
+
"NumericType",
|
540
|
+
"OffsetNumber",
|
541
|
+
"OffsetNumberDescription",
|
542
|
+
"Parameter",
|
543
|
+
"ParameterDescription",
|
544
|
+
"ParameterValues",
|
545
|
+
"patch_parameter_types",
|
546
|
+
"State",
|
547
|
+
"Switch",
|
548
|
+
"SwitchDescription",
|
549
|
+
]
|