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.
- pyplumio/__init__.py +3 -2
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +14 -14
- pyplumio/const.py +7 -0
- pyplumio/devices/__init__.py +32 -19
- pyplumio/devices/ecomax.py +112 -128
- pyplumio/devices/ecoster.py +5 -0
- pyplumio/devices/mixer.py +21 -31
- pyplumio/devices/thermostat.py +19 -29
- pyplumio/filters.py +166 -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/data_types.py +23 -21
- pyplumio/helpers/event_manager.py +40 -3
- pyplumio/helpers/factory.py +5 -2
- pyplumio/helpers/schedule.py +8 -5
- pyplumio/helpers/task_manager.py +3 -0
- pyplumio/helpers/timeout.py +8 -8
- pyplumio/helpers/uid.py +8 -5
- pyplumio/{helpers/parameter.py → parameters/__init__.py} +98 -4
- pyplumio/parameters/ecomax.py +868 -0
- pyplumio/parameters/mixer.py +245 -0
- pyplumio/parameters/thermostat.py +197 -0
- pyplumio/protocol.py +6 -3
- pyplumio/stream.py +3 -0
- pyplumio/structures/__init__.py +3 -0
- pyplumio/structures/alerts.py +8 -5
- pyplumio/structures/boiler_load.py +3 -0
- pyplumio/structures/boiler_power.py +3 -0
- pyplumio/structures/ecomax_parameters.py +6 -800
- pyplumio/structures/fan_power.py +3 -0
- pyplumio/structures/frame_versions.py +3 -0
- pyplumio/structures/fuel_consumption.py +3 -0
- pyplumio/structures/fuel_level.py +3 -0
- pyplumio/structures/lambda_sensor.py +8 -0
- pyplumio/structures/mixer_parameters.py +8 -230
- pyplumio/structures/mixer_sensors.py +9 -0
- pyplumio/structures/modules.py +14 -0
- pyplumio/structures/network_info.py +11 -0
- pyplumio/structures/output_flags.py +9 -0
- pyplumio/structures/outputs.py +21 -0
- pyplumio/structures/pending_alerts.py +3 -0
- pyplumio/structures/product_info.py +5 -2
- pyplumio/structures/program_version.py +3 -0
- pyplumio/structures/regulator_data.py +4 -1
- pyplumio/structures/regulator_data_schema.py +3 -0
- pyplumio/structures/schedules.py +18 -1
- pyplumio/structures/statuses.py +9 -0
- pyplumio/structures/temperatures.py +22 -0
- pyplumio/structures/thermostat_parameters.py +13 -177
- pyplumio/structures/thermostat_sensors.py +9 -0
- pyplumio/utils.py +14 -12
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/METADATA +30 -17
- pyplumio-0.5.43.dist-info/RECORD +63 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/WHEEL +1 -1
- pyplumio-0.5.42.dist-info/RECORD +0 -60
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/licenses/LICENSE +0 -0
- {pyplumio-0.5.42.dist-info → pyplumio-0.5.43.dist-info}/top_level.txt +0 -0
pyplumio/helpers/data_types.py
CHANGED
@@ -54,15 +54,15 @@ class DataType(ABC, Generic[T]):
|
|
54
54
|
|
55
55
|
return NotImplemented
|
56
56
|
|
57
|
-
def
|
57
|
+
def _slice_to_size(self, buffer: bytes) -> bytes:
|
58
58
|
"""Slice the data to data type size."""
|
59
|
-
return
|
59
|
+
return buffer if self.size == 0 else buffer[: self.size]
|
60
60
|
|
61
61
|
@classmethod
|
62
|
-
def from_bytes(cls: type[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(
|
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,
|
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,
|
141
|
+
def unpack(self, buffer: bytes) -> None:
|
142
142
|
"""Unpack the data."""
|
143
|
-
self._value = UnsignedChar.from_bytes(
|
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,
|
173
|
+
def unpack(self, buffer: bytes) -> None:
|
174
174
|
"""Unpack the data."""
|
175
|
-
self._value = socket.inet_ntoa(self.
|
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,
|
192
|
+
def unpack(self, buffer: bytes) -> None:
|
193
193
|
"""Unpack the data."""
|
194
|
-
self._value = socket.inet_ntop(socket.AF_INET6, self.
|
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,
|
211
|
+
def unpack(self, buffer: bytes) -> None:
|
212
212
|
"""Unpack the data."""
|
213
|
-
self._value =
|
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,
|
231
|
+
def unpack(self, buffer: bytes) -> None:
|
232
232
|
"""Unpack the data."""
|
233
|
-
self._size =
|
234
|
-
self._value =
|
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,
|
251
|
+
def unpack(self, buffer: bytes) -> None:
|
252
252
|
"""Unpack the data."""
|
253
|
-
self._size =
|
254
|
-
self._value =
|
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,
|
268
|
+
def unpack(self, buffer: bytes) -> None:
|
269
269
|
"""Unpack the data."""
|
270
|
-
self._value = self._struct.unpack_from(
|
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
|
-
|
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:
|
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"]
|
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,24 +3,21 @@
|
|
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
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
|
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
|
|
@@ -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,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
|
+
]
|