PyPlumIO 0.5.42__py3-none-any.whl → 0.5.43__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.
Files changed (60) hide show
  1. pyplumio/__init__.py +3 -2
  2. pyplumio/_version.py +2 -2
  3. pyplumio/connection.py +14 -14
  4. pyplumio/const.py +7 -0
  5. pyplumio/devices/__init__.py +32 -19
  6. pyplumio/devices/ecomax.py +112 -128
  7. pyplumio/devices/ecoster.py +5 -0
  8. pyplumio/devices/mixer.py +21 -31
  9. pyplumio/devices/thermostat.py +19 -29
  10. pyplumio/filters.py +166 -147
  11. pyplumio/frames/__init__.py +20 -8
  12. pyplumio/frames/messages.py +3 -0
  13. pyplumio/frames/requests.py +21 -0
  14. pyplumio/frames/responses.py +18 -0
  15. pyplumio/helpers/data_types.py +23 -21
  16. pyplumio/helpers/event_manager.py +40 -3
  17. pyplumio/helpers/factory.py +5 -2
  18. pyplumio/helpers/schedule.py +8 -5
  19. pyplumio/helpers/task_manager.py +3 -0
  20. pyplumio/helpers/timeout.py +8 -8
  21. pyplumio/helpers/uid.py +8 -5
  22. pyplumio/{helpers/parameter.py → parameters/__init__.py} +98 -4
  23. pyplumio/parameters/ecomax.py +868 -0
  24. pyplumio/parameters/mixer.py +245 -0
  25. pyplumio/parameters/thermostat.py +197 -0
  26. pyplumio/protocol.py +6 -3
  27. pyplumio/stream.py +3 -0
  28. pyplumio/structures/__init__.py +3 -0
  29. pyplumio/structures/alerts.py +8 -5
  30. pyplumio/structures/boiler_load.py +3 -0
  31. pyplumio/structures/boiler_power.py +3 -0
  32. pyplumio/structures/ecomax_parameters.py +6 -800
  33. pyplumio/structures/fan_power.py +3 -0
  34. pyplumio/structures/frame_versions.py +3 -0
  35. pyplumio/structures/fuel_consumption.py +3 -0
  36. pyplumio/structures/fuel_level.py +3 -0
  37. pyplumio/structures/lambda_sensor.py +8 -0
  38. pyplumio/structures/mixer_parameters.py +8 -230
  39. pyplumio/structures/mixer_sensors.py +9 -0
  40. pyplumio/structures/modules.py +14 -0
  41. pyplumio/structures/network_info.py +11 -0
  42. pyplumio/structures/output_flags.py +9 -0
  43. pyplumio/structures/outputs.py +21 -0
  44. pyplumio/structures/pending_alerts.py +3 -0
  45. pyplumio/structures/product_info.py +5 -2
  46. pyplumio/structures/program_version.py +3 -0
  47. pyplumio/structures/regulator_data.py +4 -1
  48. pyplumio/structures/regulator_data_schema.py +3 -0
  49. pyplumio/structures/schedules.py +18 -1
  50. pyplumio/structures/statuses.py +9 -0
  51. pyplumio/structures/temperatures.py +22 -0
  52. pyplumio/structures/thermostat_parameters.py +13 -177
  53. pyplumio/structures/thermostat_sensors.py +9 -0
  54. pyplumio/utils.py +14 -12
  55. {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/METADATA +30 -17
  56. pyplumio-0.5.43.dist-info/RECORD +63 -0
  57. {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/WHEEL +1 -1
  58. pyplumio-0.5.42.dist-info/RECORD +0 -60
  59. {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/licenses/LICENSE +0 -0
  60. {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/top_level.txt +0 -0
@@ -54,15 +54,15 @@ class DataType(ABC, Generic[T]):
54
54
 
55
55
  return NotImplemented
56
56
 
57
- def _slice_data(self, data: bytes) -> bytes:
57
+ def _slice_to_size(self, buffer: bytes) -> bytes:
58
58
  """Slice the data to data type size."""
59
- return data if self.size == 0 else data[: self.size]
59
+ return buffer if self.size == 0 else buffer[: self.size]
60
60
 
61
61
  @classmethod
62
- def from_bytes(cls: type[DataTypeT], data: bytes, offset: int = 0) -> DataTypeT:
62
+ def from_bytes(cls: type[DataTypeT], buffer: bytes, offset: int = 0) -> DataTypeT:
63
63
  """Initialize a new data type from bytes."""
64
64
  data_type = cls()
65
- data_type.unpack(data[offset:])
65
+ data_type.unpack(buffer[offset:])
66
66
  return data_type
67
67
 
68
68
  def to_bytes(self) -> bytes:
@@ -84,7 +84,7 @@ class DataType(ABC, Generic[T]):
84
84
  """Pack the data."""
85
85
 
86
86
  @abstractmethod
87
- def unpack(self, data: bytes) -> None:
87
+ def unpack(self, buffer: bytes) -> None:
88
88
  """Unpack the data."""
89
89
 
90
90
 
@@ -138,9 +138,9 @@ class BitArray(DataType[int]):
138
138
 
139
139
  return b""
140
140
 
141
- def unpack(self, data: bytes) -> None:
141
+ def unpack(self, buffer: bytes) -> None:
142
142
  """Unpack the data."""
143
- self._value = UnsignedChar.from_bytes(data[:1]).value
143
+ self._value = UnsignedChar.from_bytes(buffer[:1]).value
144
144
 
145
145
  @property
146
146
  def value(self) -> bool:
@@ -170,9 +170,9 @@ class IPv4(DataType[str]):
170
170
  """Pack the data."""
171
171
  return socket.inet_aton(self.value)
172
172
 
173
- def unpack(self, data: bytes) -> None:
173
+ def unpack(self, buffer: bytes) -> None:
174
174
  """Unpack the data."""
175
- self._value = socket.inet_ntoa(self._slice_data(data))
175
+ self._value = socket.inet_ntoa(self._slice_to_size(buffer))
176
176
 
177
177
 
178
178
  class IPv6(DataType[str]):
@@ -189,9 +189,9 @@ class IPv6(DataType[str]):
189
189
  """Pack the data."""
190
190
  return socket.inet_pton(socket.AF_INET6, self.value)
191
191
 
192
- def unpack(self, data: bytes) -> None:
192
+ def unpack(self, buffer: bytes) -> None:
193
193
  """Unpack the data."""
194
- self._value = socket.inet_ntop(socket.AF_INET6, self._slice_data(data))
194
+ self._value = socket.inet_ntop(socket.AF_INET6, self._slice_to_size(buffer))
195
195
 
196
196
 
197
197
  class String(DataType[str]):
@@ -208,9 +208,9 @@ class String(DataType[str]):
208
208
  """Pack the data."""
209
209
  return self.value.encode() + b"\0"
210
210
 
211
- def unpack(self, data: bytes) -> None:
211
+ def unpack(self, buffer: bytes) -> None:
212
212
  """Unpack the data."""
213
- self._value = data.split(b"\0", 1)[0].decode("utf-8", "replace")
213
+ self._value = buffer.split(b"\0", 1)[0].decode("utf-8", "replace")
214
214
  self._size = len(self.value) + 1
215
215
 
216
216
 
@@ -228,10 +228,10 @@ class VarBytes(DataType[bytes]):
228
228
  """Pack the data."""
229
229
  return UnsignedChar(self.size - 1).to_bytes() + self.value
230
230
 
231
- def unpack(self, data: bytes) -> None:
231
+ def unpack(self, buffer: bytes) -> None:
232
232
  """Unpack the data."""
233
- self._size = data[0] + 1
234
- self._value = data[1 : self.size]
233
+ self._size = buffer[0] + 1
234
+ self._value = buffer[1 : self.size]
235
235
 
236
236
 
237
237
  class VarString(DataType[str]):
@@ -248,10 +248,10 @@ class VarString(DataType[str]):
248
248
  """Pack the data."""
249
249
  return UnsignedChar(self.size - 1).to_bytes() + self.value.encode()
250
250
 
251
- def unpack(self, data: bytes) -> None:
251
+ def unpack(self, buffer: bytes) -> None:
252
252
  """Unpack the data."""
253
- self._size = data[0] + 1
254
- self._value = data[1 : self.size].decode("utf-8", "replace")
253
+ self._size = buffer[0] + 1
254
+ self._value = buffer[1 : self.size].decode("utf-8", "replace")
255
255
 
256
256
 
257
257
  class BuiltInDataType(DataType[T], ABC):
@@ -265,9 +265,9 @@ class BuiltInDataType(DataType[T], ABC):
265
265
  """Pack the data."""
266
266
  return self._struct.pack(self.value)
267
267
 
268
- def unpack(self, data: bytes) -> None:
268
+ def unpack(self, buffer: bytes) -> None:
269
269
  """Unpack the data."""
270
- self._value = self._struct.unpack_from(data)[0]
270
+ self._value = self._struct.unpack_from(buffer)[0]
271
271
 
272
272
  @property
273
273
  def size(self) -> int:
@@ -379,3 +379,5 @@ DATA_TYPES: tuple[type[DataType], ...] = (
379
379
  IPv4,
380
380
  IPv6,
381
381
  )
382
+
383
+ __all__ = ["DataType"] + list({dt.__name__ for dt in DATA_TYPES})
@@ -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,27 @@ 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
- CallbackT = TypeVar("CallbackT", bound=Callback)
15
+
16
+ _CallableT: TypeAlias = Callable[..., Any]
17
+ _CallbackT = TypeVar("_CallbackT", bound=Callback)
18
+
19
+
20
+ def event_listener(event: str, filter: _CallableT | None = None) -> _CallableT:
21
+ """Mark a function as an event listener.
22
+
23
+ This decorator attaches metadata to the function, identifying it
24
+ as a subscriber for the specified event.
25
+ """
26
+
27
+ def decorator(func: _CallbackT) -> _CallbackT:
28
+ # Attach metadata to the function to mark it as a listener.
29
+ setattr(func, "_on_event", event)
30
+ setattr(func, "_on_event_filter", filter)
31
+ return func
32
+
33
+ return decorator
34
+
35
+
15
36
  T = TypeVar("T")
16
37
 
17
38
 
@@ -30,6 +51,7 @@ class EventManager(TaskManager, Generic[T]):
30
51
  self.data = {}
31
52
  self._events = {}
32
53
  self._callbacks = {}
54
+ self._register_event_listeners()
33
55
 
34
56
  def __getattr__(self, name: str) -> T:
35
57
  """Return attributes from the underlying data dictionary."""
@@ -38,6 +60,18 @@ class EventManager(TaskManager, Generic[T]):
38
60
  except KeyError as e:
39
61
  raise AttributeError from e
40
62
 
63
+ def _register_event_listeners(self) -> None:
64
+ """Register the event listeners."""
65
+ for event, callback in self.event_listeners():
66
+ filter_func = getattr(callback, "_on_event_filter", None)
67
+ self.subscribe(event, filter_func(callback) if filter_func else callback)
68
+
69
+ def event_listeners(self) -> Generator[tuple[str, Callback]]:
70
+ """Get the event listeners."""
71
+ for _, callback in inspect.getmembers(self, predicate=inspect.ismethod):
72
+ if event := getattr(callback, "_on_event", None):
73
+ yield (event, callback)
74
+
41
75
  async def wait_for(self, name: str, timeout: float | None = None) -> None:
42
76
  """Wait for the value to become available.
43
77
 
@@ -91,7 +125,7 @@ class EventManager(TaskManager, Generic[T]):
91
125
  except KeyError:
92
126
  return default
93
127
 
94
- def subscribe(self, name: str, callback: CallbackT) -> CallbackT:
128
+ def subscribe(self, name: str, callback: _CallbackT) -> _CallbackT:
95
129
  """Subscribe a callback to the event.
96
130
 
97
131
  :param name: Event name or ID
@@ -189,3 +223,6 @@ class EventManager(TaskManager, Generic[T]):
189
223
  def events(self) -> dict[str, asyncio.Event]:
190
224
  """Return the events."""
191
225
  return self._events
226
+
227
+
228
+ __all__ = ["Callback", "EventManager", "event_listener"]
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
13
13
  T = TypeVar("T")
14
14
 
15
15
 
16
- async def _import_module(name: str) -> ModuleType:
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 _import_module(module_name)
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"]
@@ -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 _get_time(
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 _get_time_range(start: Time, end: Time, step: dt.timedelta = STEP) -> list[Time]:
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 [_get_time(index, start=start_dt, step=step) for index in range(int(steps))]
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 _get_time_range(start, end):
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({_get_time(index): state for index, state in enumerate(intervals)})
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"]
@@ -40,3 +40,6 @@ class TaskManager:
40
40
  def tasks(self) -> set[asyncio.Task]:
41
41
  """Return the tasks."""
42
42
  return self._tasks
43
+
44
+
45
+ __all__ = ["TaskManager"]
@@ -3,24 +3,21 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Awaitable, Callable, Coroutine
6
+ from collections.abc import Awaitable, Callable
7
7
  from functools import wraps
8
8
  from typing import Any, TypeVar
9
9
 
10
- from typing_extensions import ParamSpec
10
+ from typing_extensions import ParamSpec, TypeAlias
11
11
 
12
12
  T = TypeVar("T")
13
13
  P = ParamSpec("P")
14
+ _CallableT: TypeAlias = Callable[..., Any]
14
15
 
15
16
 
16
- def timeout(
17
- seconds: float,
18
- ) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Coroutine[Any, Any, T]]]:
17
+ def timeout(seconds: float) -> _CallableT:
19
18
  """Decorate a timeout for the awaitable."""
20
19
 
21
- def decorator(
22
- func: Callable[P, Awaitable[T]],
23
- ) -> Callable[P, Coroutine[Any, Any, T]]:
20
+ def decorator(func: Callable[P, Awaitable[T]]) -> _CallableT:
24
21
  @wraps(func)
25
22
  async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
26
23
  return await asyncio.wait_for(func(*args, **kwargs), timeout=seconds)
@@ -28,3 +25,6 @@ def timeout(
28
25
  return wrapper
29
26
 
30
27
  return decorator
28
+
29
+
30
+ __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 _base5(buffer + _crc16(buffer))
15
+ return base5(buffer + crc16(buffer))
16
16
 
17
17
 
18
- def _base5(buffer: bytes) -> str:
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 _crc16(buffer: bytes) -> bytes:
29
+ def crc16(buffer: bytes) -> bytes:
30
30
  """Return a CRC 16."""
31
- crc16 = reduce(_crc16_byte, buffer, CRC)
31
+ crc16 = reduce(crc16_byte, buffer, CRC)
32
32
  return crc16.to_bytes(length=2, byteorder="little")
33
33
 
34
34
 
35
- def _crc16_byte(crc: int, byte: int) -> int:
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 BYTE_UNDEFINED, STATE_OFF, STATE_ON, State, UnitOfMeasurement
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
 
@@ -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(f"Invalid switch value: {value}. Must be 'on' or 'off'.")
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,60 @@ 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 type-checking
513
+ 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
+ parameter_types[index] = replacements[description.name] # type: ignore[assignment]
526
+
527
+ return parameter_types
528
+
529
+
530
+ __all__ = [
531
+ "Number",
532
+ "NumberDescription",
533
+ "NumericType",
534
+ "OffsetNumber",
535
+ "OffsetNumberDescription",
536
+ "Parameter",
537
+ "ParameterDescription",
538
+ "ParameterValues",
539
+ "patch_parameter_types",
540
+ "State",
541
+ "Switch",
542
+ "SwitchDescription",
543
+ ]