PyPlumIO 0.6.0__py3-none-any.whl → 0.6.2__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 (49) hide show
  1. pyplumio/__init__.py +3 -1
  2. pyplumio/_version.py +2 -2
  3. pyplumio/connection.py +0 -36
  4. pyplumio/const.py +0 -5
  5. pyplumio/data_types.py +2 -2
  6. pyplumio/devices/__init__.py +23 -5
  7. pyplumio/devices/ecomax.py +30 -53
  8. pyplumio/devices/ecoster.py +2 -3
  9. pyplumio/filters.py +199 -136
  10. pyplumio/frames/__init__.py +101 -15
  11. pyplumio/frames/messages.py +8 -65
  12. pyplumio/frames/requests.py +38 -38
  13. pyplumio/frames/responses.py +30 -86
  14. pyplumio/helpers/async_cache.py +13 -8
  15. pyplumio/helpers/event_manager.py +24 -18
  16. pyplumio/helpers/factory.py +0 -3
  17. pyplumio/parameters/__init__.py +38 -35
  18. pyplumio/protocol.py +63 -47
  19. pyplumio/structures/alerts.py +2 -2
  20. pyplumio/structures/ecomax_parameters.py +1 -1
  21. pyplumio/structures/frame_versions.py +3 -2
  22. pyplumio/structures/mixer_parameters.py +5 -3
  23. pyplumio/structures/network_info.py +1 -0
  24. pyplumio/structures/product_info.py +1 -1
  25. pyplumio/structures/program_version.py +2 -2
  26. pyplumio/structures/schedules.py +8 -40
  27. pyplumio/structures/sensor_data.py +498 -0
  28. pyplumio/structures/thermostat_parameters.py +7 -4
  29. pyplumio/utils.py +41 -4
  30. {pyplumio-0.6.0.dist-info → pyplumio-0.6.2.dist-info}/METADATA +7 -8
  31. pyplumio-0.6.2.dist-info/RECORD +50 -0
  32. pyplumio/structures/boiler_load.py +0 -32
  33. pyplumio/structures/boiler_power.py +0 -33
  34. pyplumio/structures/fan_power.py +0 -33
  35. pyplumio/structures/fuel_consumption.py +0 -36
  36. pyplumio/structures/fuel_level.py +0 -39
  37. pyplumio/structures/lambda_sensor.py +0 -57
  38. pyplumio/structures/mixer_sensors.py +0 -80
  39. pyplumio/structures/modules.py +0 -102
  40. pyplumio/structures/output_flags.py +0 -47
  41. pyplumio/structures/outputs.py +0 -88
  42. pyplumio/structures/pending_alerts.py +0 -28
  43. pyplumio/structures/statuses.py +0 -52
  44. pyplumio/structures/temperatures.py +0 -94
  45. pyplumio/structures/thermostat_sensors.py +0 -106
  46. pyplumio-0.6.0.dist-info/RECORD +0 -63
  47. {pyplumio-0.6.0.dist-info → pyplumio-0.6.2.dist-info}/WHEEL +0 -0
  48. {pyplumio-0.6.0.dist-info → pyplumio-0.6.2.dist-info}/licenses/LICENSE +0 -0
  49. {pyplumio-0.6.0.dist-info → pyplumio-0.6.2.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from typing import Any
6
6
 
7
7
  from pyplumio.const import ATTR_PASSWORD, FrameType
8
- from pyplumio.frames import Response
8
+ from pyplumio.frames import Response, Structured, frame_handler
9
9
  from pyplumio.structures.alerts import AlertsStructure
10
10
  from pyplumio.structures.ecomax_parameters import EcomaxParametersStructure
11
11
  from pyplumio.structures.mixer_parameters import MixerParametersStructure
@@ -17,19 +17,15 @@ from pyplumio.structures.schedules import SchedulesStructure
17
17
  from pyplumio.structures.thermostat_parameters import ThermostatParametersStructure
18
18
 
19
19
 
20
- class AlertsResponse(Response):
20
+ @frame_handler(FrameType.RESPONSE_ALERTS, structure=AlertsStructure)
21
+ class AlertsResponse(Structured, Response):
21
22
  """Represents response to a device alerts request."""
22
23
 
23
24
  __slots__ = ()
24
25
 
25
- frame_type = FrameType.RESPONSE_ALERTS
26
26
 
27
- def decode_message(self, message: bytearray) -> dict[str, Any]:
28
- """Decode a frame message."""
29
- return AlertsStructure(self).decode(message)[0]
30
-
31
-
32
- class DeviceAvailableResponse(Response):
27
+ @frame_handler(FrameType.RESPONSE_DEVICE_AVAILABLE, structure=NetworkInfoStructure)
28
+ class DeviceAvailableResponse(Structured, Response):
33
29
  """Represents a device available response.
34
30
 
35
31
  Contains network information and status.
@@ -37,17 +33,8 @@ class DeviceAvailableResponse(Response):
37
33
 
38
34
  __slots__ = ()
39
35
 
40
- frame_type = FrameType.RESPONSE_DEVICE_AVAILABLE
41
-
42
- def create_message(self, data: dict[str, Any]) -> bytearray:
43
- """Create a frame message."""
44
- return NetworkInfoStructure(self).encode(data)
45
-
46
- def decode_message(self, message: bytearray) -> dict[str, Any]:
47
- """Decode a frame message."""
48
- return NetworkInfoStructure(self).decode(message, offset=1)[0]
49
-
50
36
 
37
+ @frame_handler(FrameType.RESPONSE_ECOMAX_CONTROL)
51
38
  class EcomaxControlResponse(Response):
52
39
  """Represents response to an ecoMAX control request.
53
40
 
@@ -57,10 +44,11 @@ class EcomaxControlResponse(Response):
57
44
 
58
45
  __slots__ = ()
59
46
 
60
- frame_type = FrameType.RESPONSE_ECOMAX_CONTROL
61
47
 
62
-
63
- class EcomaxParametersResponse(Response):
48
+ @frame_handler(
49
+ FrameType.RESPONSE_ECOMAX_PARAMETERS, structure=EcomaxParametersStructure
50
+ )
51
+ class EcomaxParametersResponse(Structured, Response):
64
52
  """Represents an ecoMAX parameters response.
65
53
 
66
54
  Contains editable ecoMAX parameters.
@@ -68,14 +56,9 @@ class EcomaxParametersResponse(Response):
68
56
 
69
57
  __slots__ = ()
70
58
 
71
- frame_type = FrameType.RESPONSE_ECOMAX_PARAMETERS
72
-
73
- def decode_message(self, message: bytearray) -> dict[str, Any]:
74
- """Decode a frame message."""
75
- return EcomaxParametersStructure(self).decode(message)[0]
76
-
77
59
 
78
- class MixerParametersResponse(Response):
60
+ @frame_handler(FrameType.RESPONSE_MIXER_PARAMETERS, structure=MixerParametersStructure)
61
+ class MixerParametersResponse(Structured, Response):
79
62
  """Represents a mixer parameters response.
80
63
 
81
64
  Contains editable mixer parameters.
@@ -83,13 +66,8 @@ class MixerParametersResponse(Response):
83
66
 
84
67
  __slots__ = ()
85
68
 
86
- frame_type = FrameType.RESPONSE_MIXER_PARAMETERS
87
-
88
- def decode_message(self, message: bytearray) -> dict[str, Any]:
89
- """Decode a frame message."""
90
- return MixerParametersStructure(self).decode(message)[0]
91
-
92
69
 
70
+ @frame_handler(FrameType.RESPONSE_PASSWORD)
93
71
  class PasswordResponse(Response):
94
72
  """Represents a password response.
95
73
 
@@ -98,15 +76,14 @@ class PasswordResponse(Response):
98
76
 
99
77
  __slots__ = ()
100
78
 
101
- frame_type = FrameType.RESPONSE_PASSWORD
102
-
103
79
  def decode_message(self, message: bytearray) -> dict[str, Any]:
104
80
  """Decode a frame message."""
105
81
  password = message[1:].decode() if message[1:] else None
106
82
  return {ATTR_PASSWORD: password}
107
83
 
108
84
 
109
- class ProgramVersionResponse(Response):
85
+ @frame_handler(FrameType.RESPONSE_PROGRAM_VERSION, structure=ProgramVersionStructure)
86
+ class ProgramVersionResponse(Structured, Response):
110
87
  """Represents a program version response.
111
88
 
112
89
  Contains software version info.
@@ -114,18 +91,11 @@ class ProgramVersionResponse(Response):
114
91
 
115
92
  __slots__ = ()
116
93
 
117
- frame_type = FrameType.RESPONSE_PROGRAM_VERSION
118
-
119
- def create_message(self, data: dict[str, Any]) -> bytearray:
120
- """Create a frame message."""
121
- return ProgramVersionStructure(self).encode(data)
122
-
123
- def decode_message(self, message: bytearray) -> dict[str, Any]:
124
- """Decode a frame message."""
125
- return ProgramVersionStructure(self).decode(message)[0]
126
-
127
94
 
128
- class RegulatorDataSchemaResponse(Response):
95
+ @frame_handler(
96
+ FrameType.RESPONSE_REGULATOR_DATA_SCHEMA, structure=RegulatorDataSchemaStructure
97
+ )
98
+ class RegulatorDataSchemaResponse(Structured, Response):
129
99
  """Represents a regulator data schema response.
130
100
 
131
101
  Contains schema, that describes structure of ecoMAX regulator data
@@ -134,25 +104,15 @@ class RegulatorDataSchemaResponse(Response):
134
104
 
135
105
  __slots__ = ()
136
106
 
137
- frame_type = FrameType.RESPONSE_REGULATOR_DATA_SCHEMA
138
-
139
- def decode_message(self, message: bytearray) -> dict[str, Any]:
140
- """Decode a frame message."""
141
- return RegulatorDataSchemaStructure(self).decode(message)[0]
142
-
143
107
 
144
- class SchedulesResponse(Response):
108
+ @frame_handler(FrameType.RESPONSE_SCHEDULES, structure=SchedulesStructure)
109
+ class SchedulesResponse(Structured, Response):
145
110
  """Represents response to a device schedules request."""
146
111
 
147
112
  __slots__ = ()
148
113
 
149
- frame_type = FrameType.RESPONSE_SCHEDULES
150
-
151
- def decode_message(self, message: bytearray) -> dict[str, Any]:
152
- """Decode a frame message."""
153
- return SchedulesStructure(self).decode(message)[0]
154
-
155
114
 
115
+ @frame_handler(FrameType.RESPONSE_SET_ECOMAX_PARAMETER)
156
116
  class SetEcomaxParameterResponse(Response):
157
117
  """Represents response to a set ecoMAX parameter request.
158
118
 
@@ -162,9 +122,8 @@ class SetEcomaxParameterResponse(Response):
162
122
 
163
123
  __slots__ = ()
164
124
 
165
- frame_type = FrameType.RESPONSE_SET_ECOMAX_PARAMETER
166
-
167
125
 
126
+ @frame_handler(FrameType.RESPONSE_SET_MIXER_PARAMETER)
168
127
  class SetMixerParameterResponse(Response):
169
128
  """Represents response to a set mixer parameter request.
170
129
 
@@ -174,9 +133,8 @@ class SetMixerParameterResponse(Response):
174
133
 
175
134
  __slots__ = ()
176
135
 
177
- frame_type = FrameType.RESPONSE_SET_MIXER_PARAMETER
178
-
179
136
 
137
+ @frame_handler(FrameType.RESPONSE_SET_THERMOSTAT_PARAMETER)
180
138
  class SetThermostatParameterResponse(Response):
181
139
  """Represents response to a set thermostat parameter request.
182
140
 
@@ -186,10 +144,11 @@ class SetThermostatParameterResponse(Response):
186
144
 
187
145
  __slots__ = ()
188
146
 
189
- frame_type = FrameType.RESPONSE_SET_THERMOSTAT_PARAMETER
190
-
191
147
 
192
- class ThermostatParametersResponse(Response):
148
+ @frame_handler(
149
+ FrameType.RESPONSE_THERMOSTAT_PARAMETERS, structure=ThermostatParametersStructure
150
+ )
151
+ class ThermostatParametersResponse(Structured, Response):
193
152
  """Represents a thermostat parameters response.
194
153
 
195
154
  Contains editable thermostat parameters.
@@ -197,14 +156,9 @@ class ThermostatParametersResponse(Response):
197
156
 
198
157
  __slots__ = ()
199
158
 
200
- frame_type = FrameType.RESPONSE_THERMOSTAT_PARAMETERS
201
-
202
- def decode_message(self, message: bytearray) -> dict[str, Any]:
203
- """Decode a frame message."""
204
- return ThermostatParametersStructure(self).decode(message)[0]
205
-
206
159
 
207
- class UIDResponse(Response):
160
+ @frame_handler(FrameType.RESPONSE_UID, structure=ProductInfoStructure)
161
+ class UIDResponse(Structured, Response):
208
162
  """Represents an UID response.
209
163
 
210
164
  Contains product info and product UID.
@@ -212,16 +166,6 @@ class UIDResponse(Response):
212
166
 
213
167
  __slots__ = ()
214
168
 
215
- frame_type = FrameType.RESPONSE_UID
216
-
217
- def create_message(self, data: dict[str, Any]) -> bytearray:
218
- """Create a frame message."""
219
- return ProductInfoStructure(self).encode(data)
220
-
221
- def decode_message(self, message: bytearray) -> dict[str, Any]:
222
- """Decode a frame message."""
223
- return ProductInfoStructure(self).decode(message)[0]
224
-
225
169
 
226
170
  __all__ = [
227
171
  "AlertsResponse",
@@ -2,30 +2,35 @@
2
2
 
3
3
  from collections.abc import Awaitable, Callable
4
4
  from functools import wraps
5
- from typing import Any, ParamSpec, TypeAlias, TypeVar, cast
5
+ from types import MappingProxyType
6
+ from typing import Any, ParamSpec, TypeVar, cast
6
7
 
7
8
  T = TypeVar("T")
8
9
  P = ParamSpec("P")
9
- _CallableT: TypeAlias = Callable[..., Awaitable[Any]]
10
10
 
11
11
 
12
12
  class AsyncCache:
13
13
  """A simple cache for asynchronous functions."""
14
14
 
15
- __slots__ = ("cache",)
15
+ __slots__ = ("_cache",)
16
16
 
17
- cache: dict[str, Any]
17
+ _cache: dict[str, Any]
18
18
 
19
19
  def __init__(self) -> None:
20
20
  """Initialize the cache."""
21
- self.cache = {}
21
+ self._cache = {}
22
22
 
23
- async def get(self, key: str, coro: _CallableT) -> Any:
23
+ async def get(self, key: str, coro: Callable[..., Awaitable[Any]]) -> Any:
24
24
  """Get a value from the cache or compute and store it."""
25
25
  if key not in self.cache:
26
- self.cache[key] = await coro()
26
+ self._cache[key] = await coro()
27
27
 
28
- return self.cache[key]
28
+ return self._cache[key]
29
+
30
+ @property
31
+ def cache(self) -> MappingProxyType[str, Any]:
32
+ """Return the internal cache dictionary."""
33
+ return MappingProxyType(self._cache)
29
34
 
30
35
 
31
36
  # Create a global cache instance
@@ -5,34 +5,35 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  from collections.abc import Callable, Coroutine, Generator
7
7
  import inspect
8
+ from types import MappingProxyType
8
9
  from typing import Any, Generic, TypeAlias, TypeVar, overload
9
10
 
10
11
  from pyplumio.helpers.task_manager import TaskManager
11
12
 
12
- Callback: TypeAlias = Callable[[Any], Coroutine[Any, Any, Any]]
13
+ EventCallback: TypeAlias = Callable[[Any], Coroutine[Any, Any, Any]]
14
+ _Callable: TypeAlias = Callable[..., Any]
13
15
 
14
- _CallableT: TypeAlias = Callable[..., Any]
15
- _CallbackT = TypeVar("_CallbackT", bound=Callback)
16
+ _EventCallbackT = TypeVar("_EventCallbackT", bound=EventCallback)
16
17
 
17
18
 
18
19
  @overload
19
- def event_listener(name: _CallableT, filter: None = None) -> Callback: ...
20
+ def event_listener(name: _Callable, filter: None = None) -> EventCallback: ...
20
21
 
21
22
 
22
23
  @overload
23
24
  def event_listener(
24
- name: str | None = None, filter: _CallableT | None = None
25
- ) -> _CallableT: ...
25
+ name: str | None = None, filter: _Callable | None = None
26
+ ) -> _Callable: ...
26
27
 
27
28
 
28
- def event_listener(name: Any = None, filter: _CallableT | None = None) -> Any:
29
+ def event_listener(name: Any = None, filter: _Callable | None = None) -> Any:
29
30
  """Mark a function as an event listener.
30
31
 
31
32
  This decorator attaches metadata to the function, identifying it
32
33
  as a subscriber for the specified event.
33
34
  """
34
35
 
35
- def decorator(func: _CallbackT) -> _CallbackT:
36
+ def decorator(func: _EventCallbackT) -> _EventCallbackT:
36
37
  # Attach metadata to the function to mark it as a listener.
37
38
  event = (
38
39
  name
@@ -55,16 +56,16 @@ T = TypeVar("T")
55
56
  class EventManager(TaskManager, Generic[T]):
56
57
  """Represents an event manager."""
57
58
 
58
- __slots__ = ("data", "_events", "_callbacks")
59
+ __slots__ = ("_data", "_events", "_callbacks")
59
60
 
60
- data: dict[str, T]
61
+ _data: dict[str, T]
61
62
  _events: dict[str, asyncio.Event]
62
- _callbacks: dict[str, list[Callback]]
63
+ _callbacks: dict[str, list[EventCallback]]
63
64
 
64
65
  def __init__(self) -> None:
65
66
  """Initialize a new event manager."""
66
67
  super().__init__()
67
- self.data = {}
68
+ self._data = {}
68
69
  self._events = {}
69
70
  self._callbacks = {}
70
71
  self._register_event_listeners()
@@ -82,7 +83,7 @@ class EventManager(TaskManager, Generic[T]):
82
83
  filter_func = getattr(callback, "_on_event_filter", None)
83
84
  self.subscribe(event, filter_func(callback) if filter_func else callback)
84
85
 
85
- def event_listeners(self) -> Generator[tuple[str, Callback]]:
86
+ def event_listeners(self) -> Generator[tuple[str, EventCallback]]:
86
87
  """Get the event listeners."""
87
88
  for _, callback in inspect.getmembers(self, predicate=inspect.ismethod):
88
89
  if event := getattr(callback, "_on_event", None):
@@ -141,7 +142,7 @@ class EventManager(TaskManager, Generic[T]):
141
142
  except KeyError:
142
143
  return default
143
144
 
144
- def subscribe(self, name: str, callback: _CallbackT) -> _CallbackT:
145
+ def subscribe(self, name: str, callback: _EventCallbackT) -> _EventCallbackT:
145
146
  """Subscribe a callback to the event.
146
147
 
147
148
  :param name: Event name or ID
@@ -157,7 +158,7 @@ class EventManager(TaskManager, Generic[T]):
157
158
  callbacks.append(callback)
158
159
  return callback
159
160
 
160
- def subscribe_once(self, name: str, callback: Callback) -> Callback:
161
+ def subscribe_once(self, name: str, callback: EventCallback) -> EventCallback:
161
162
  """Subscribe a callback to the event once.
162
163
 
163
164
  Callback will be unsubscribed after single event.
@@ -179,7 +180,7 @@ class EventManager(TaskManager, Generic[T]):
179
180
 
180
181
  return self.subscribe(name, _call_once)
181
182
 
182
- def unsubscribe(self, name: str, callback: Callback) -> bool:
183
+ def unsubscribe(self, name: str, callback: EventCallback) -> bool:
183
184
  """Usubscribe a callback from the event.
184
185
 
185
186
  :param name: Event name or ID
@@ -204,7 +205,7 @@ class EventManager(TaskManager, Generic[T]):
204
205
  if (result := await callback(value)) is not None:
205
206
  value = result
206
207
 
207
- self.data[name] = value
208
+ self._data[name] = value
208
209
  self.set_event(name)
209
210
 
210
211
  def dispatch_nowait(self, name: str, value: T) -> None:
@@ -240,5 +241,10 @@ class EventManager(TaskManager, Generic[T]):
240
241
  """Return the events."""
241
242
  return self._events
242
243
 
244
+ @property
245
+ def data(self) -> MappingProxyType[str, T]:
246
+ """Return the event data."""
247
+ return MappingProxyType(self._data)
248
+
243
249
 
244
- __all__ = ["Callback", "EventManager", "event_listener"]
250
+ __all__ = ["EventCallback", "EventManager", "event_listener"]
@@ -4,14 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import importlib
7
- import logging
8
7
  from types import ModuleType
9
8
  from typing import Any, TypeVar
10
9
 
11
10
  from pyplumio.helpers.async_cache import acache
12
11
 
13
- _LOGGER = logging.getLogger(__name__)
14
-
15
12
  T = TypeVar("T")
16
13
 
17
14
 
@@ -5,7 +5,8 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
  import asyncio
7
7
  from contextlib import suppress
8
- from dataclasses import asdict, dataclass
8
+ from copy import copy
9
+ from dataclasses import dataclass, replace
9
10
  import logging
10
11
  from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar, get_args
11
12
 
@@ -18,7 +19,7 @@ if TYPE_CHECKING:
18
19
 
19
20
  _LOGGER = logging.getLogger(__name__)
20
21
 
21
- ParameterT = TypeVar("ParameterT", bound="Parameter")
22
+ _ParameterT = TypeVar("_ParameterT", bound="Parameter")
22
23
 
23
24
 
24
25
  def unpack_parameter(
@@ -44,7 +45,7 @@ def is_valid_parameter(data: bytearray) -> bool:
44
45
  return any(x for x in data if x != BYTE_UNDEFINED)
45
46
 
46
47
 
47
- @dataclass(slots=True)
48
+ @dataclass(slots=True, frozen=True)
48
49
  class ParameterValues:
49
50
  """Represents a parameter values."""
50
51
 
@@ -61,7 +62,7 @@ class ParameterDescription:
61
62
  optimistic: bool = False
62
63
 
63
64
 
64
- NumericType: TypeAlias = int | float
65
+ Numeric: TypeAlias = int | float
65
66
 
66
67
 
67
68
  class Parameter(ABC):
@@ -111,31 +112,34 @@ class Parameter(ABC):
111
112
 
112
113
  def __hash__(self) -> int:
113
114
  """Return a hash of the parameter based on its values."""
114
- return hash(frozenset(asdict(self.values).items()))
115
+ return hash(self.values)
115
116
 
116
117
  def _call_relational_method(self, method_to_call: str, other: Any) -> Any:
117
118
  """Call a specified relational method."""
119
+ handler = getattr(self.values.value, method_to_call)
118
120
  if isinstance(other, Parameter):
119
- other = other.values
121
+ return handler(other.values.value)
120
122
 
121
123
  if isinstance(other, ParameterValues):
122
- handler = getattr(self.values.value, method_to_call)
123
124
  return handler(other.value)
124
125
 
125
- if isinstance(other, int | float | bool) or other in get_args(State):
126
- handler = getattr(self.values.value, method_to_call)
126
+ if isinstance(other, Numeric | bool) or other in get_args(State):
127
127
  return handler(self._pack_value(other))
128
- else:
129
- return NotImplemented
128
+
129
+ return NotImplemented
130
130
 
131
131
  def __int__(self) -> int:
132
132
  """Return an integer representation of parameter's value."""
133
133
  return self.values.value
134
134
 
135
135
  def __add__(self, other: Any) -> Any:
136
- """Return result of addition."""
136
+ """Add a number to this parameter."""
137
137
  return self._call_relational_method("__add__", other)
138
138
 
139
+ def __radd__(self, other: Any) -> Any:
140
+ """Add this parameter to another number."""
141
+ return self._call_relational_method("__radd__", other)
142
+
139
143
  def __sub__(self, other: Any) -> Any:
140
144
  """Return result of the subtraction."""
141
145
  return self._call_relational_method("__sub__", other)
@@ -174,10 +178,7 @@ class Parameter(ABC):
174
178
 
175
179
  def __copy__(self) -> Parameter:
176
180
  """Create a copy of parameter."""
177
- values = type(self.values)(
178
- self.values.value, self.values.min_value, self.values.max_value
179
- )
180
- return type(self)(self.device, self.description, values)
181
+ return type(self)(self.device, self.description, values=copy(self.values))
181
182
 
182
183
  async def set(self, value: Any, retries: int = 0, timeout: float = 5.0) -> bool:
183
184
  """Set a parameter value."""
@@ -200,7 +201,7 @@ class Parameter(ABC):
200
201
  # Value is unchanged
201
202
  return True
202
203
 
203
- self._values.value = value
204
+ self._values = replace(self._values, value=value)
204
205
  request = await self.create_request()
205
206
  if self.description.optimistic:
206
207
  await self.device.queue.put(request)
@@ -262,14 +263,14 @@ class Parameter(ABC):
262
263
 
263
264
  @classmethod
264
265
  def create_or_update(
265
- cls: type[ParameterT],
266
+ cls: type[_ParameterT],
266
267
  device: Device,
267
268
  description: ParameterDescription,
268
269
  values: ParameterValues,
269
270
  **kwargs: Any,
270
- ) -> ParameterT:
271
+ ) -> _ParameterT:
271
272
  """Create new parameter or update parameter values."""
272
- parameter: ParameterT | None = device.get_nowait(description.name, None)
273
+ parameter: _ParameterT | None = device.get_nowait(description.name, None)
273
274
  if parameter and isinstance(parameter, cls):
274
275
  parameter.update(values)
275
276
  else:
@@ -293,17 +294,17 @@ class Parameter(ABC):
293
294
 
294
295
  @property
295
296
  @abstractmethod
296
- def value(self) -> NumericType | State | bool:
297
+ def value(self) -> Numeric | State | bool:
297
298
  """Return the value."""
298
299
 
299
300
  @property
300
301
  @abstractmethod
301
- def min_value(self) -> NumericType | State | bool:
302
+ def min_value(self) -> Numeric | State | bool:
302
303
  """Return the minimum allowed value."""
303
304
 
304
305
  @property
305
306
  @abstractmethod
306
- def max_value(self) -> NumericType | State | bool:
307
+ def max_value(self) -> Numeric | State | bool:
307
308
  """Return the maximum allowed value."""
308
309
 
309
310
  @abstractmethod
@@ -327,11 +328,15 @@ class Number(Parameter):
327
328
 
328
329
  description: NumberDescription
329
330
 
330
- def _pack_value(self, value: NumericType) -> int:
331
+ def __float__(self) -> float:
332
+ """Return number value as float."""
333
+ return float(self.value)
334
+
335
+ def _pack_value(self, value: Numeric) -> int:
331
336
  """Pack the parameter value."""
332
337
  return int(round(value / self.description.step))
333
338
 
334
- def _unpack_value(self, value: int) -> NumericType:
339
+ def _unpack_value(self, value: int) -> Numeric:
335
340
  """Unpack the parameter value."""
336
341
  return round(value * self.description.step, self.description.precision)
337
342
 
@@ -351,14 +356,12 @@ class Number(Parameter):
351
356
 
352
357
  return True
353
358
 
354
- async def set(
355
- self, value: NumericType, retries: int = 0, timeout: float = 5.0
356
- ) -> bool:
359
+ async def set(self, value: Numeric, retries: int = 0, timeout: float = 5.0) -> bool:
357
360
  """Set a parameter value."""
358
361
  return await super().set(value, retries=retries, timeout=timeout)
359
362
 
360
363
  def set_nowait(
361
- self, value: NumericType, retries: int = 0, timeout: float = 5.0
364
+ self, value: Numeric, retries: int = 0, timeout: float = 5.0
362
365
  ) -> None:
363
366
  """Set a parameter value without waiting."""
364
367
  super().set_nowait(value, retries=retries, timeout=timeout)
@@ -368,17 +371,17 @@ class Number(Parameter):
368
371
  return Request()
369
372
 
370
373
  @property
371
- def value(self) -> NumericType:
374
+ def value(self) -> Numeric:
372
375
  """Return the value."""
373
376
  return self._unpack_value(self.values.value)
374
377
 
375
378
  @property
376
- def min_value(self) -> NumericType:
379
+ def min_value(self) -> Numeric:
377
380
  """Return the minimum allowed value."""
378
381
  return self._unpack_value(self.values.min_value)
379
382
 
380
383
  @property
381
- def max_value(self) -> NumericType:
384
+ def max_value(self) -> Numeric:
382
385
  """Return the maximum allowed value."""
383
386
  return self._unpack_value(self.values.max_value)
384
387
 
@@ -402,11 +405,11 @@ class OffsetNumber(Number):
402
405
 
403
406
  description: OffsetNumberDescription
404
407
 
405
- def _pack_value(self, value: NumericType) -> int:
408
+ def _pack_value(self, value: Numeric) -> int:
406
409
  """Pack the parameter value."""
407
410
  return super()._pack_value(value + self.description.offset)
408
411
 
409
- def _unpack_value(self, value: int) -> NumericType:
412
+ def _unpack_value(self, value: int) -> Numeric:
410
413
  """Unpack the parameter value."""
411
414
  return super()._unpack_value(value - self.description.offset)
412
415
 
@@ -505,7 +508,7 @@ class Switch(Parameter):
505
508
  __all__ = [
506
509
  "Number",
507
510
  "NumberDescription",
508
- "NumericType",
511
+ "Numeric",
509
512
  "OffsetNumber",
510
513
  "OffsetNumberDescription",
511
514
  "Parameter",