PyPlumIO 0.5.25__py3-none-any.whl → 0.5.26__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-0.5.25.dist-info → PyPlumIO-0.5.26.dist-info}/METADATA +7 -7
- PyPlumIO-0.5.26.dist-info/RECORD +60 -0
- {PyPlumIO-0.5.25.dist-info → PyPlumIO-0.5.26.dist-info}/WHEEL +1 -1
- pyplumio/_version.py +2 -2
- pyplumio/devices/__init__.py +27 -28
- pyplumio/devices/ecomax.py +51 -57
- pyplumio/devices/ecoster.py +3 -5
- pyplumio/devices/mixer.py +5 -8
- pyplumio/devices/thermostat.py +3 -3
- pyplumio/filters.py +6 -6
- pyplumio/frames/__init__.py +6 -6
- pyplumio/frames/messages.py +3 -3
- pyplumio/frames/requests.py +18 -18
- pyplumio/frames/responses.py +15 -15
- pyplumio/helpers/data_types.py +104 -68
- pyplumio/helpers/event_manager.py +7 -7
- pyplumio/helpers/factory.py +5 -6
- pyplumio/helpers/parameter.py +17 -15
- pyplumio/helpers/schedule.py +50 -46
- pyplumio/helpers/timeout.py +1 -1
- pyplumio/protocol.py +6 -7
- pyplumio/structures/alerts.py +8 -6
- pyplumio/structures/ecomax_parameters.py +30 -26
- pyplumio/structures/frame_versions.py +2 -3
- pyplumio/structures/mixer_parameters.py +9 -6
- pyplumio/structures/mixer_sensors.py +10 -8
- pyplumio/structures/modules.py +9 -7
- pyplumio/structures/network_info.py +16 -16
- pyplumio/structures/program_version.py +3 -0
- pyplumio/structures/regulator_data.py +2 -4
- pyplumio/structures/regulator_data_schema.py +2 -3
- pyplumio/structures/schedules.py +33 -35
- pyplumio/structures/thermostat_parameters.py +6 -4
- pyplumio/structures/thermostat_sensors.py +13 -10
- PyPlumIO-0.5.25.dist-info/RECORD +0 -60
- {PyPlumIO-0.5.25.dist-info → PyPlumIO-0.5.26.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.25.dist-info → PyPlumIO-0.5.26.dist-info}/top_level.txt +0 -0
@@ -60,6 +60,7 @@ class EventManager(TaskManager, Generic[T]):
|
|
60
60
|
become available, defaults to `None`
|
61
61
|
:type timeout: float, optional
|
62
62
|
:return: An event data
|
63
|
+
:rtype: T
|
63
64
|
:raise asyncio.TimeoutError: when waiting past specified timeout
|
64
65
|
"""
|
65
66
|
await self.wait_for(name, timeout=timeout)
|
@@ -81,8 +82,9 @@ class EventManager(TaskManager, Generic[T]):
|
|
81
82
|
:type name: str
|
82
83
|
:param default: default value to return if data is unavailable,
|
83
84
|
defaults to `None`
|
84
|
-
:type default:
|
85
|
+
:type default: T, optional
|
85
86
|
:return: An event data
|
87
|
+
:rtype: T, optional
|
86
88
|
"""
|
87
89
|
try:
|
88
90
|
return self.data[name]
|
@@ -148,9 +150,9 @@ class EventManager(TaskManager, Generic[T]):
|
|
148
150
|
async def dispatch(self, name: str, value: T) -> None:
|
149
151
|
"""Call registered callbacks and dispatch the event."""
|
150
152
|
if callbacks := self._callbacks.get(name, None):
|
151
|
-
for callback in
|
152
|
-
result
|
153
|
-
|
153
|
+
for callback in callbacks:
|
154
|
+
if (result := await callback(value)) is not None:
|
155
|
+
value = result
|
154
156
|
|
155
157
|
self.data[name] = value
|
156
158
|
self.set_event(name)
|
@@ -181,9 +183,7 @@ class EventManager(TaskManager, Generic[T]):
|
|
181
183
|
def set_event(self, name: str) -> None:
|
182
184
|
"""Set an event."""
|
183
185
|
if name in self.events:
|
184
|
-
|
185
|
-
if not event.is_set():
|
186
|
-
event.set()
|
186
|
+
self.events[name].set()
|
187
187
|
|
188
188
|
@property
|
189
189
|
def events(self) -> dict[str, asyncio.Event]:
|
pyplumio/helpers/factory.py
CHANGED
@@ -13,18 +13,17 @@ _LOGGER = logging.getLogger(__name__)
|
|
13
13
|
T = TypeVar("T")
|
14
14
|
|
15
15
|
|
16
|
-
async def
|
17
|
-
"""
|
18
|
-
|
19
|
-
|
20
|
-
)
|
16
|
+
async def _import_module(name: str) -> ModuleType:
|
17
|
+
"""Import module by name."""
|
18
|
+
loop = asyncio.get_running_loop()
|
19
|
+
return await loop.run_in_executor(None, importlib.import_module, f"pyplumio.{name}")
|
21
20
|
|
22
21
|
|
23
22
|
async def create_instance(class_path: str, cls: type[T], **kwargs: Any) -> T:
|
24
23
|
"""Return a class instance from the class path."""
|
25
24
|
module_name, class_name = class_path.rsplit(".", 1)
|
26
25
|
try:
|
27
|
-
module = await
|
26
|
+
module = await _import_module(module_name)
|
28
27
|
instance = getattr(module, class_name)(**kwargs)
|
29
28
|
if not isinstance(instance, cls):
|
30
29
|
raise TypeError(f"class '{class_name}' should be derived from {cls}")
|
pyplumio/helpers/parameter.py
CHANGED
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
|
|
6
6
|
import asyncio
|
7
7
|
from dataclasses import dataclass
|
8
8
|
import logging
|
9
|
-
from typing import TYPE_CHECKING, Any,
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union
|
10
10
|
|
11
11
|
from dataslots import dataslots
|
12
12
|
from typing_extensions import TypeAlias
|
@@ -19,10 +19,8 @@ if TYPE_CHECKING:
|
|
19
19
|
|
20
20
|
_LOGGER = logging.getLogger(__name__)
|
21
21
|
|
22
|
-
SET_TIMEOUT: Final = 5
|
23
|
-
SET_RETRIES: Final = 5
|
24
22
|
|
25
|
-
|
23
|
+
ParameterValue: TypeAlias = Union[int, float, bool, Literal["off", "on"]]
|
26
24
|
ParameterT = TypeVar("ParameterT", bound="Parameter")
|
27
25
|
|
28
26
|
|
@@ -49,7 +47,7 @@ def check_parameter(data: bytearray) -> bool:
|
|
49
47
|
return any(x for x in data if x != BYTE_UNDEFINED)
|
50
48
|
|
51
49
|
|
52
|
-
def _normalize_parameter_value(value:
|
50
|
+
def _normalize_parameter_value(value: ParameterValue) -> int:
|
53
51
|
"""Normalize a parameter value."""
|
54
52
|
if value in (STATE_OFF, STATE_ON):
|
55
53
|
return 1 if value == STATE_ON else 0
|
@@ -177,7 +175,7 @@ class Parameter(ABC):
|
|
177
175
|
)
|
178
176
|
return type(self)(self.device, self.description, values)
|
179
177
|
|
180
|
-
async def set(self, value: Any, retries: int =
|
178
|
+
async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
|
181
179
|
"""Set a parameter value."""
|
182
180
|
if (value := _normalize_parameter_value(value)) == self.values.value:
|
183
181
|
return True
|
@@ -198,7 +196,7 @@ class Parameter(ABC):
|
|
198
196
|
return False
|
199
197
|
|
200
198
|
await self.device.queue.put(await self.create_request())
|
201
|
-
await asyncio.sleep(
|
199
|
+
await asyncio.sleep(timeout)
|
202
200
|
retries -= 1
|
203
201
|
|
204
202
|
return True
|
@@ -272,13 +270,17 @@ class Number(Parameter):
|
|
272
270
|
|
273
271
|
description: NumberDescription
|
274
272
|
|
275
|
-
async def set(
|
273
|
+
async def set(
|
274
|
+
self, value: int | float, retries: int = 5, timeout: float = 5.0
|
275
|
+
) -> bool:
|
276
276
|
"""Set a parameter value."""
|
277
|
-
return await super().set(value, retries)
|
277
|
+
return await super().set(value, retries, timeout)
|
278
278
|
|
279
|
-
def set_nowait(
|
279
|
+
def set_nowait(
|
280
|
+
self, value: int | float, retries: int = 5, timeout: float = 5.0
|
281
|
+
) -> None:
|
280
282
|
"""Set a parameter value without waiting."""
|
281
|
-
self.device.create_task(self.set(value, retries))
|
283
|
+
self.device.create_task(self.set(value, retries, timeout))
|
282
284
|
|
283
285
|
async def create_request(self) -> Request:
|
284
286
|
"""Create a request to change the number."""
|
@@ -319,16 +321,16 @@ class Switch(Parameter):
|
|
319
321
|
description: SwitchDescription
|
320
322
|
|
321
323
|
async def set(
|
322
|
-
self, value: bool | Literal["off", "on"], retries: int =
|
324
|
+
self, value: bool | Literal["off", "on"], retries: int = 5, timeout: float = 5.0
|
323
325
|
) -> bool:
|
324
326
|
"""Set a parameter value."""
|
325
|
-
return await super().set(value, retries)
|
327
|
+
return await super().set(value, retries, timeout)
|
326
328
|
|
327
329
|
def set_nowait(
|
328
|
-
self, value: bool | Literal["off", "on"], retries: int =
|
330
|
+
self, value: bool | Literal["off", "on"], retries: int = 5, timeout: float = 5.0
|
329
331
|
) -> None:
|
330
332
|
"""Set a switch value without waiting."""
|
331
|
-
self.device.create_task(self.set(value, retries))
|
333
|
+
self.device.create_task(self.set(value, retries, timeout))
|
332
334
|
|
333
335
|
async def turn_on(self) -> bool:
|
334
336
|
"""Set a switch value to 'on'.
|
pyplumio/helpers/schedule.py
CHANGED
@@ -2,53 +2,65 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
from collections.abc import Iterable, Iterator, MutableMapping
|
5
|
+
from collections.abc import Generator, Iterable, Iterator, MutableMapping
|
6
6
|
from dataclasses import dataclass
|
7
7
|
import datetime as dt
|
8
8
|
from functools import lru_cache
|
9
9
|
import math
|
10
|
-
from typing import Final, Literal
|
10
|
+
from typing import Annotated, Final, Literal, get_args
|
11
|
+
|
12
|
+
from typing_extensions import TypeAlias
|
11
13
|
|
12
14
|
from pyplumio.const import STATE_OFF, STATE_ON, FrameType
|
13
|
-
from pyplumio.devices import
|
15
|
+
from pyplumio.devices import PhysicalDevice
|
14
16
|
from pyplumio.frames import Request
|
15
17
|
from pyplumio.structures.schedules import collect_schedule_data
|
16
18
|
|
17
19
|
TIME_FORMAT: Final = "%H:%M"
|
18
|
-
START_OF_DAY: Final = "00:00"
|
19
|
-
END_OF_DAY: Final = "00:00"
|
20
20
|
|
21
21
|
STATE_NIGHT: Final = "night"
|
22
22
|
STATE_DAY: Final = "day"
|
23
23
|
|
24
24
|
ON_STATES: Final = (STATE_ON, STATE_DAY)
|
25
25
|
OFF_STATES: Final = (STATE_OFF, STATE_NIGHT)
|
26
|
-
ALLOWED_STATES: Final = ON_STATES + OFF_STATES
|
27
26
|
|
27
|
+
ScheduleState: TypeAlias = Literal["on", "off", "day", "night"]
|
28
|
+
Time = Annotated[str, "time in HH:MM format"]
|
29
|
+
|
30
|
+
start_of_day_dt = dt.datetime.strptime("00:00", TIME_FORMAT)
|
28
31
|
|
29
|
-
@lru_cache(maxsize=10)
|
30
|
-
def _parse_interval(start: str, end: str) -> tuple[int, int]:
|
31
|
-
"""Parse an interval string.
|
32
32
|
|
33
|
-
|
33
|
+
def _get_time_range(
|
34
|
+
start: Time, end: Time, step: int = 30
|
35
|
+
) -> Generator[int, None, None]:
|
36
|
+
"""Get a time range.
|
37
|
+
|
38
|
+
Start and end times should be specified in HH:MM format, step in
|
39
|
+
minutes.
|
34
40
|
"""
|
35
|
-
start_dt = dt.datetime.strptime(start, TIME_FORMAT)
|
36
|
-
end_dt = dt.datetime.strptime(end, TIME_FORMAT)
|
37
|
-
start_of_day_dt = dt.datetime.strptime(START_OF_DAY, TIME_FORMAT)
|
38
|
-
if end_dt == start_of_day_dt:
|
39
|
-
# Upper bound of interval is midnight.
|
40
|
-
end_dt += dt.timedelta(hours=23, minutes=30)
|
41
|
-
|
42
|
-
if end_dt <= start_dt:
|
43
|
-
raise ValueError(
|
44
|
-
f"Invalid interval ({start}, {end}). "
|
45
|
-
+ "Lower boundary must be less than upper."
|
46
|
-
)
|
47
41
|
|
48
|
-
|
49
|
-
|
42
|
+
@lru_cache(maxsize=10)
|
43
|
+
def _get_time_range_cached(start: Time, end: Time, step: int = 30) -> range:
|
44
|
+
"""Get a time range and cache it using LRU cache."""
|
45
|
+
start_dt = dt.datetime.strptime(start, TIME_FORMAT)
|
46
|
+
end_dt = dt.datetime.strptime(end, TIME_FORMAT)
|
47
|
+
if end_dt == start_of_day_dt:
|
48
|
+
# Upper boundary of the interval is midnight.
|
49
|
+
end_dt += dt.timedelta(hours=24) - dt.timedelta(minutes=step)
|
50
|
+
|
51
|
+
if end_dt <= start_dt:
|
52
|
+
raise ValueError(
|
53
|
+
f"Invalid interval ({start}, {end}). "
|
54
|
+
"Lower boundary must be less than upper."
|
55
|
+
)
|
56
|
+
|
57
|
+
def _dt_to_index(dt: dt.datetime) -> int:
|
58
|
+
"""Convert datetime to index in schedule list."""
|
59
|
+
return math.floor((dt - start_of_day_dt).total_seconds() // (60 * step))
|
60
|
+
|
61
|
+
return range(_dt_to_index(start_dt), _dt_to_index(end_dt) + 1)
|
50
62
|
|
51
|
-
|
63
|
+
yield from _get_time_range_cached(start, end, step)
|
52
64
|
|
53
65
|
|
54
66
|
class ScheduleDay(MutableMapping):
|
@@ -91,30 +103,21 @@ class ScheduleDay(MutableMapping):
|
|
91
103
|
self._intervals.append(item)
|
92
104
|
|
93
105
|
def set_state(
|
94
|
-
self,
|
95
|
-
state: Literal["on", "off", "day", "night"],
|
96
|
-
start: str = START_OF_DAY,
|
97
|
-
end: str = END_OF_DAY,
|
106
|
+
self, state: ScheduleState, start: Time = "00:00", end: Time = "00:00"
|
98
107
|
) -> None:
|
99
|
-
"""Set
|
100
|
-
|
101
|
-
Can be on of the following:
|
102
|
-
'on', 'off', 'day' or 'night'.
|
103
|
-
"""
|
104
|
-
if state not in ALLOWED_STATES:
|
108
|
+
"""Set a schedule interval state."""
|
109
|
+
if state not in get_args(ScheduleState):
|
105
110
|
raise ValueError(f'state "{state}" is not allowed')
|
106
111
|
|
107
|
-
index
|
108
|
-
|
109
|
-
self._intervals[index] = state in ON_STATES
|
110
|
-
index += 1
|
112
|
+
for index in _get_time_range(start, end):
|
113
|
+
self._intervals[index] = True if state in ON_STATES else False
|
111
114
|
|
112
|
-
def set_on(self, start:
|
113
|
-
"""Set
|
115
|
+
def set_on(self, start: Time = "00:00", end: Time = "00:00") -> None:
|
116
|
+
"""Set a schedule interval state to 'on'."""
|
114
117
|
self.set_state(STATE_ON, start, end)
|
115
118
|
|
116
|
-
def set_off(self, start:
|
117
|
-
"""Set
|
119
|
+
def set_off(self, start: Time = "00:00", end: Time = "00:00") -> None:
|
120
|
+
"""Set a schedule interval state to 'off'."""
|
118
121
|
self.set_state(STATE_OFF, start, end)
|
119
122
|
|
120
123
|
@property
|
@@ -130,24 +133,25 @@ class Schedule(Iterable):
|
|
130
133
|
__slots__ = (
|
131
134
|
"name",
|
132
135
|
"device",
|
136
|
+
"sunday",
|
133
137
|
"monday",
|
134
138
|
"tuesday",
|
135
139
|
"wednesday",
|
136
140
|
"thursday",
|
137
141
|
"friday",
|
138
142
|
"saturday",
|
139
|
-
"sunday",
|
140
143
|
)
|
141
144
|
|
142
145
|
name: str
|
143
|
-
device:
|
146
|
+
device: PhysicalDevice
|
147
|
+
|
148
|
+
sunday: ScheduleDay
|
144
149
|
monday: ScheduleDay
|
145
150
|
tuesday: ScheduleDay
|
146
151
|
wednesday: ScheduleDay
|
147
152
|
thursday: ScheduleDay
|
148
153
|
friday: ScheduleDay
|
149
154
|
saturday: ScheduleDay
|
150
|
-
sunday: ScheduleDay
|
151
155
|
|
152
156
|
def __iter__(self) -> Iterator[ScheduleDay]:
|
153
157
|
"""Return list of days."""
|
pyplumio/helpers/timeout.py
CHANGED
pyplumio/protocol.py
CHANGED
@@ -11,7 +11,7 @@ import logging
|
|
11
11
|
from typing_extensions import TypeAlias
|
12
12
|
|
13
13
|
from pyplumio.const import ATTR_CONNECTED, DeviceType
|
14
|
-
from pyplumio.devices import
|
14
|
+
from pyplumio.devices import PhysicalDevice
|
15
15
|
from pyplumio.exceptions import ProtocolError
|
16
16
|
from pyplumio.frames import Frame
|
17
17
|
from pyplumio.frames.requests import StartMasterRequest
|
@@ -113,7 +113,7 @@ class Queues:
|
|
113
113
|
await asyncio.gather(self.read.join(), self.write.join())
|
114
114
|
|
115
115
|
|
116
|
-
class AsyncProtocol(Protocol, EventManager[
|
116
|
+
class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
|
117
117
|
"""Represents an async protocol.
|
118
118
|
|
119
119
|
This protocol implements producer-consumers pattern using
|
@@ -204,7 +204,7 @@ class AsyncProtocol(Protocol, EventManager[AddressableDevice]):
|
|
204
204
|
await writer.write(await queues.write.get())
|
205
205
|
queues.write.task_done()
|
206
206
|
|
207
|
-
if
|
207
|
+
if response := await reader.read():
|
208
208
|
queues.read.put_nowait(response)
|
209
209
|
|
210
210
|
except ProtocolError as e:
|
@@ -224,16 +224,15 @@ class AsyncProtocol(Protocol, EventManager[AddressableDevice]):
|
|
224
224
|
device.handle_frame(frame)
|
225
225
|
queue.task_done()
|
226
226
|
|
227
|
-
async def get_device_entry(self, device_type: DeviceType) ->
|
227
|
+
async def get_device_entry(self, device_type: DeviceType) -> PhysicalDevice:
|
228
228
|
"""Set up or return a device entry."""
|
229
229
|
name = device_type.name.lower()
|
230
230
|
if name not in self.data:
|
231
|
-
device = await
|
231
|
+
device = await PhysicalDevice.create(
|
232
232
|
device_type, queue=self._queues.write, network=self._network
|
233
233
|
)
|
234
234
|
device.dispatch_nowait(ATTR_CONNECTED, True)
|
235
235
|
self.create_task(device.async_setup(), name=f"device_setup_task ({name})")
|
236
|
-
self.
|
237
|
-
self.data[name] = device
|
236
|
+
await self.dispatch(name, device)
|
238
237
|
|
239
238
|
return self.data[name]
|
pyplumio/structures/alerts.py
CHANGED
@@ -76,12 +76,13 @@ class AlertsStructure(StructureDecoder):
|
|
76
76
|
|
77
77
|
def _unpack_alert(self, message: bytearray) -> Alert:
|
78
78
|
"""Unpack an alert."""
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
79
|
+
offset = self._offset
|
80
|
+
code = message[offset]
|
81
|
+
offset += 1
|
82
|
+
from_seconds = UnsignedInt.from_bytes(message, offset)
|
83
|
+
offset += from_seconds.size
|
84
|
+
to_seconds = UnsignedInt.from_bytes(message, offset)
|
85
|
+
offset += to_seconds.size
|
85
86
|
from_dt = _seconds_to_datetime(from_seconds.value)
|
86
87
|
to_dt = (
|
87
88
|
None
|
@@ -91,6 +92,7 @@ class AlertsStructure(StructureDecoder):
|
|
91
92
|
with suppress(ValueError):
|
92
93
|
code = AlertType(code)
|
93
94
|
|
95
|
+
self._offset = offset
|
94
96
|
return Alert(code, from_dt, to_dt)
|
95
97
|
|
96
98
|
def decode(
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from collections.abc import Generator
|
6
6
|
from dataclasses import dataclass
|
7
|
+
from functools import partial
|
7
8
|
from typing import Any, Final
|
8
9
|
|
9
10
|
from dataslots import dataslots
|
@@ -18,10 +19,9 @@ from pyplumio.const import (
|
|
18
19
|
ProductType,
|
19
20
|
UnitOfMeasurement,
|
20
21
|
)
|
21
|
-
from pyplumio.devices import
|
22
|
+
from pyplumio.devices import PhysicalDevice
|
22
23
|
from pyplumio.frames import Request
|
23
24
|
from pyplumio.helpers.parameter import (
|
24
|
-
SET_TIMEOUT,
|
25
25
|
Number,
|
26
26
|
NumberDescription,
|
27
27
|
Parameter,
|
@@ -47,41 +47,40 @@ class EcomaxParameterDescription(ParameterDescription):
|
|
47
47
|
|
48
48
|
__slots__ = ()
|
49
49
|
|
50
|
-
multiplier: float = 1.0
|
51
|
-
offset: int = 0
|
52
|
-
|
53
50
|
|
54
51
|
class EcomaxParameter(Parameter):
|
55
52
|
"""Represents an ecoMAX parameter."""
|
56
53
|
|
57
54
|
__slots__ = ()
|
58
55
|
|
59
|
-
device:
|
56
|
+
device: PhysicalDevice
|
60
57
|
description: EcomaxParameterDescription
|
61
58
|
|
62
59
|
async def create_request(self) -> Request:
|
63
60
|
"""Create a request to change the parameter."""
|
61
|
+
handler = partial(Request.create, recipient=self.device.address)
|
64
62
|
if self.description.name == ATTR_ECOMAX_CONTROL:
|
65
|
-
|
66
|
-
|
63
|
+
request = await handler(
|
64
|
+
frame_type=FrameType.REQUEST_ECOMAX_CONTROL,
|
65
|
+
data={ATTR_VALUE: self.values.value},
|
66
|
+
)
|
67
67
|
elif self.description.name == ATTR_THERMOSTAT_PROFILE:
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
68
|
+
request = await handler(
|
69
|
+
frame_type=FrameType.REQUEST_SET_THERMOSTAT_PARAMETER,
|
70
|
+
data={
|
71
|
+
ATTR_INDEX: self._index,
|
72
|
+
ATTR_VALUE: self.values.value,
|
73
|
+
ATTR_OFFSET: 0,
|
74
|
+
ATTR_SIZE: 1,
|
75
|
+
},
|
76
|
+
)
|
75
77
|
else:
|
76
|
-
|
77
|
-
|
78
|
-
ATTR_INDEX: self._index,
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
return await Request.create(
|
83
|
-
frame_type, recipient=self.device.address, data=data
|
84
|
-
)
|
78
|
+
request = await handler(
|
79
|
+
frame_type=FrameType.REQUEST_SET_ECOMAX_PARAMETER,
|
80
|
+
data={ATTR_INDEX: self._index, ATTR_VALUE: self.values.value},
|
81
|
+
)
|
82
|
+
|
83
|
+
return request
|
85
84
|
|
86
85
|
|
87
86
|
@dataslots
|
@@ -89,6 +88,9 @@ class EcomaxParameter(Parameter):
|
|
89
88
|
class EcomaxNumberDescription(EcomaxParameterDescription, NumberDescription):
|
90
89
|
"""Represents an ecoMAX number description."""
|
91
90
|
|
91
|
+
multiplier: float = 1.0
|
92
|
+
offset: int = 0
|
93
|
+
|
92
94
|
|
93
95
|
class EcomaxNumber(EcomaxParameter, Number):
|
94
96
|
"""Represents a ecoMAX number."""
|
@@ -97,10 +99,12 @@ class EcomaxNumber(EcomaxParameter, Number):
|
|
97
99
|
|
98
100
|
description: EcomaxNumberDescription
|
99
101
|
|
100
|
-
async def set(
|
102
|
+
async def set(
|
103
|
+
self, value: float | int, retries: int = 5, timeout: float = 5.0
|
104
|
+
) -> bool:
|
101
105
|
"""Set a parameter value."""
|
102
106
|
value = (value + self.description.offset) / self.description.multiplier
|
103
|
-
return await super().set(value, retries)
|
107
|
+
return await super().set(value, retries, timeout)
|
104
108
|
|
105
109
|
@property
|
106
110
|
def value(self) -> float:
|
@@ -23,9 +23,8 @@ class FrameVersionsStructure(StructureDecoder):
|
|
23
23
|
def _unpack_frame_versions(self, message: bytearray) -> tuple[FrameType | int, int]:
|
24
24
|
"""Unpack frame versions."""
|
25
25
|
frame_type = message[self._offset]
|
26
|
-
self._offset
|
27
|
-
|
28
|
-
self._offset += version.size
|
26
|
+
version = UnsignedShort.from_bytes(message, self._offset + 1)
|
27
|
+
self._offset += version.size + 1
|
29
28
|
with suppress(ValueError):
|
30
29
|
frame_type = FrameType(frame_type)
|
31
30
|
|
@@ -18,7 +18,6 @@ from pyplumio.const import (
|
|
18
18
|
)
|
19
19
|
from pyplumio.frames import Request
|
20
20
|
from pyplumio.helpers.parameter import (
|
21
|
-
SET_RETRIES,
|
22
21
|
Number,
|
23
22
|
NumberDescription,
|
24
23
|
Parameter,
|
@@ -45,9 +44,6 @@ class MixerParameterDescription(ParameterDescription):
|
|
45
44
|
|
46
45
|
__slots__ = ()
|
47
46
|
|
48
|
-
multiplier: float = 1.0
|
49
|
-
offset: int = 0
|
50
|
-
|
51
47
|
|
52
48
|
class MixerParameter(Parameter):
|
53
49
|
"""Represent a mixer parameter."""
|
@@ -75,6 +71,9 @@ class MixerParameter(Parameter):
|
|
75
71
|
class MixerNumberDescription(MixerParameterDescription, NumberDescription):
|
76
72
|
"""Represent a mixer number description."""
|
77
73
|
|
74
|
+
multiplier: float = 1.0
|
75
|
+
offset: int = 0
|
76
|
+
|
78
77
|
|
79
78
|
class MixerNumber(MixerParameter, Number):
|
80
79
|
"""Represents a mixer number."""
|
@@ -83,10 +82,12 @@ class MixerNumber(MixerParameter, Number):
|
|
83
82
|
|
84
83
|
description: MixerNumberDescription
|
85
84
|
|
86
|
-
async def set(
|
85
|
+
async def set(
|
86
|
+
self, value: int | float, retries: int = 5, timeout: float = 5.0
|
87
|
+
) -> bool:
|
87
88
|
"""Set a parameter value."""
|
88
89
|
value = (value + self.description.offset) / self.description.multiplier
|
89
|
-
return await super().set(value, retries)
|
90
|
+
return await super().set(value, retries, timeout)
|
90
91
|
|
91
92
|
@property
|
92
93
|
def value(self) -> float:
|
@@ -264,6 +265,8 @@ MIXER_PARAMETERS: dict[ProductType, tuple[MixerParameterDescription, ...]] = {
|
|
264
265
|
class MixerParametersStructure(StructureDecoder):
|
265
266
|
"""Represents a mixer parameters data structure."""
|
266
267
|
|
268
|
+
__slots__ = ("_offset",)
|
269
|
+
|
267
270
|
_offset: int
|
268
271
|
|
269
272
|
def _mixer_parameter(
|
@@ -28,18 +28,20 @@ class MixerSensorsStructure(StructureDecoder):
|
|
28
28
|
|
29
29
|
def _unpack_mixer_sensors(self, message: bytearray) -> dict[str, Any] | None:
|
30
30
|
"""Unpack sensors for a mixer."""
|
31
|
-
|
31
|
+
offset = self._offset
|
32
|
+
current_temp = Float.from_bytes(message, offset)
|
32
33
|
try:
|
33
|
-
|
34
|
-
|
34
|
+
return (
|
35
|
+
{
|
35
36
|
ATTR_CURRENT_TEMP: current_temp.value,
|
36
|
-
ATTR_TARGET_TEMP: message[
|
37
|
-
ATTR_PUMP: bool(message[
|
37
|
+
ATTR_TARGET_TEMP: message[offset + 4],
|
38
|
+
ATTR_PUMP: bool(message[offset + 6] & 0x01),
|
38
39
|
}
|
39
|
-
|
40
|
-
|
40
|
+
if not math.isnan(current_temp.value)
|
41
|
+
else None
|
42
|
+
)
|
41
43
|
finally:
|
42
|
-
self._offset
|
44
|
+
self._offset = offset + MIXER_SENSOR_SIZE
|
43
45
|
|
44
46
|
def _mixer_sensors(
|
45
47
|
self, message: bytearray, mixers: int
|
pyplumio/structures/modules.py
CHANGED
@@ -6,6 +6,8 @@ from dataclasses import dataclass
|
|
6
6
|
import struct
|
7
7
|
from typing import Any, Final
|
8
8
|
|
9
|
+
from dataslots import dataslots
|
10
|
+
|
9
11
|
from pyplumio.const import BYTE_UNDEFINED
|
10
12
|
from pyplumio.structures import StructureDecoder
|
11
13
|
from pyplumio.utils import ensure_dict
|
@@ -30,6 +32,7 @@ struct_version = struct.Struct("<BBB")
|
|
30
32
|
struct_vendor = struct.Struct("<BB")
|
31
33
|
|
32
34
|
|
35
|
+
@dataslots
|
33
36
|
@dataclass
|
34
37
|
class ConnectedModules:
|
35
38
|
"""Represents a firmware version info for connected module."""
|
@@ -55,17 +58,16 @@ class ModulesStructure(StructureDecoder):
|
|
55
58
|
self._offset += 1
|
56
59
|
return None
|
57
60
|
|
58
|
-
|
61
|
+
offset = self._offset
|
62
|
+
version_data = struct_version.unpack_from(message, offset)
|
59
63
|
version = ".".join(str(i) for i in version_data)
|
60
|
-
|
61
|
-
|
64
|
+
offset += struct_version.size
|
62
65
|
if module == ATTR_MODULE_A:
|
63
|
-
vendor_code, vendor_version = struct_vendor.unpack_from(
|
64
|
-
message, self._offset
|
65
|
-
)
|
66
|
+
vendor_code, vendor_version = struct_vendor.unpack_from(message, offset)
|
66
67
|
version += f".{chr(vendor_code) + str(vendor_version)}"
|
67
|
-
|
68
|
+
offset += struct_vendor.size
|
68
69
|
|
70
|
+
self._offset = offset
|
69
71
|
return version
|
70
72
|
|
71
73
|
def decode(
|