PyPlumIO 0.5.35__py3-none-any.whl → 0.5.36__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.35.dist-info → PyPlumIO-0.5.36.dist-info}/METADATA +6 -6
- {PyPlumIO-0.5.35.dist-info → PyPlumIO-0.5.36.dist-info}/RECORD +17 -17
- pyplumio/__init__.py +1 -0
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +1 -1
- pyplumio/devices/__init__.py +7 -7
- pyplumio/devices/ecomax.py +32 -24
- pyplumio/devices/mixer.py +4 -7
- pyplumio/exceptions.py +11 -4
- pyplumio/frames/requests.py +65 -65
- pyplumio/frames/responses.py +62 -62
- pyplumio/helpers/parameter.py +42 -47
- pyplumio/helpers/schedule.py +2 -3
- pyplumio/helpers/uid.py +5 -5
- {PyPlumIO-0.5.35.dist-info → PyPlumIO-0.5.36.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.35.dist-info → PyPlumIO-0.5.36.dist-info}/WHEEL +0 -0
- {PyPlumIO-0.5.35.dist-info → PyPlumIO-0.5.36.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.36
|
4
4
|
Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
5
5
|
Author-email: Denis Paavilainen <denpa@denpa.pro>
|
6
6
|
License: MIT License
|
@@ -27,22 +27,22 @@ Requires-Dist: dataslots==1.2.0
|
|
27
27
|
Requires-Dist: pyserial-asyncio==0.6
|
28
28
|
Requires-Dist: typing-extensions==4.12.2
|
29
29
|
Provides-Extra: test
|
30
|
-
Requires-Dist: codespell==2.
|
30
|
+
Requires-Dist: codespell==2.4.0; extra == "test"
|
31
31
|
Requires-Dist: coverage==7.6.10; extra == "test"
|
32
32
|
Requires-Dist: mypy==1.14.1; extra == "test"
|
33
33
|
Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
|
34
34
|
Requires-Dist: pytest==8.3.4; extra == "test"
|
35
35
|
Requires-Dist: pytest-asyncio==0.25.2; extra == "test"
|
36
|
-
Requires-Dist: ruff==0.9.
|
37
|
-
Requires-Dist: tox==4.
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
36
|
+
Requires-Dist: ruff==0.9.3; extra == "test"
|
37
|
+
Requires-Dist: tox==4.24.1; extra == "test"
|
38
|
+
Requires-Dist: types-pyserial==3.5.0.20250124; extra == "test"
|
39
39
|
Provides-Extra: docs
|
40
40
|
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
41
41
|
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
42
42
|
Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
|
43
43
|
Provides-Extra: dev
|
44
44
|
Requires-Dist: pyplumio[docs,test]; extra == "dev"
|
45
|
-
Requires-Dist: pre-commit==4.0
|
45
|
+
Requires-Dist: pre-commit==4.1.0; extra == "dev"
|
46
46
|
Requires-Dist: tomli==2.2.1; extra == "dev"
|
47
47
|
|
48
48
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
@@ -1,32 +1,32 @@
|
|
1
|
-
pyplumio/__init__.py,sha256=
|
1
|
+
pyplumio/__init__.py,sha256=3ibJ43RIdfFrWp1PAsQixybAA--NPRw43B5OdLOwsU8,3319
|
2
2
|
pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
|
3
|
-
pyplumio/_version.py,sha256=
|
4
|
-
pyplumio/connection.py,sha256
|
3
|
+
pyplumio/_version.py,sha256=gzGL-qBNqAXMnFJc3Sr0_AdQLS33_rosqhjOurMUizQ,413
|
4
|
+
pyplumio/connection.py,sha256=-dbrIK6ewoYNeBQod9ZmXT8JkxMKbcS6nosINFsg9RI,5972
|
5
5
|
pyplumio/const.py,sha256=LyXa5aVy2KxnZq7H7F8s5SYsAgEC2UzZYMMRauliB2E,5502
|
6
|
-
pyplumio/exceptions.py,sha256=
|
6
|
+
pyplumio/exceptions.py,sha256=_B_0EgxDxd2XyYv3WpZM733q0cML5m6J-f55QOvYRpI,996
|
7
7
|
pyplumio/filters.py,sha256=AMW1zHQ1YjJfHX7e87Dhv7AGixJ3y9Vn-_JAQn7vIsg,12526
|
8
8
|
pyplumio/protocol.py,sha256=VRxrj8vZ1FMawqblKkyxg_V61TBSvVynd9u0JXYnMUU,8090
|
9
9
|
pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
10
|
pyplumio/stream.py,sha256=Ne-mWkO6FpILAjGdagbAh_VL3QEla-eDiT2N-kOc5o4,4883
|
11
11
|
pyplumio/utils.py,sha256=TnBzRopinyp92wruguijxcIYmaeyNVTFX0dygI5FCMU,823
|
12
|
-
pyplumio/devices/__init__.py,sha256=
|
13
|
-
pyplumio/devices/ecomax.py,sha256=
|
12
|
+
pyplumio/devices/__init__.py,sha256=Erjd3DeEop_yelnLtRRaPbwMIuD1NwVh7dMM1_2KxtI,8155
|
13
|
+
pyplumio/devices/ecomax.py,sha256=hb4QULpyg5KrCWeQDNnwy7IA9k5oRETK5AWllVgA0Kg,15806
|
14
14
|
pyplumio/devices/ecoster.py,sha256=jNWli7ye9T6yfkcFJZhhUHH7KOv-L6AgYFp_dKyv3OM,263
|
15
|
-
pyplumio/devices/mixer.py,sha256=
|
15
|
+
pyplumio/devices/mixer.py,sha256=HdJNsvX3obYyLsuDhERX4IkodX3hGv3veP9ymjQnoUk,3108
|
16
16
|
pyplumio/devices/thermostat.py,sha256=-CZNRyywoDU6csFu85KSmQ5woVXY0x6peXkeOsi_fqg,2617
|
17
17
|
pyplumio/frames/__init__.py,sha256=30ECFT_5IneUrpOJGxjHyeuX-i4S1ikX8Pg1HO8Yxkg,7686
|
18
18
|
pyplumio/frames/messages.py,sha256=iDwZOPdVOZaIcEHYnkwtCazH_N6BjyEDtiJBjTRaePY,3570
|
19
|
-
pyplumio/frames/requests.py,sha256=
|
20
|
-
pyplumio/frames/responses.py,sha256=
|
19
|
+
pyplumio/frames/requests.py,sha256=X9P0TdCd8z7pf2XspuontEJsLy00EZfxxue6pg-_MT8,6854
|
20
|
+
pyplumio/frames/responses.py,sha256=dzrL0Yx7SoJuJAQyjOE8_ARfy7yvOqk2uq4kdnH5t1U,6228
|
21
21
|
pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
|
22
22
|
pyplumio/helpers/data_types.py,sha256=nB3afOLmppgSCWkZoX1-1yWPNMMNSem77x7XQ1Mi8H8,9103
|
23
23
|
pyplumio/helpers/event_manager.py,sha256=xQOfiP_nP1Pz5zhB6HU5gXyyJXjhisYshL8_HRxDgt8,6412
|
24
24
|
pyplumio/helpers/factory.py,sha256=v07s9DyihfkNUzt7ndyJbNd_DLS8UpRkut_xkGrbi6c,1123
|
25
|
-
pyplumio/helpers/parameter.py,sha256=
|
26
|
-
pyplumio/helpers/schedule.py,sha256=
|
25
|
+
pyplumio/helpers/parameter.py,sha256=ubqgleTPT-m3yxhJkQWoLjkbjCdwOeaNhdA7O66l47Q,12425
|
26
|
+
pyplumio/helpers/schedule.py,sha256=0lkghnnpQRdRtgqoNv7PnHMYYJpJNMHl9PR4_SaHB8w,5374
|
27
27
|
pyplumio/helpers/task_manager.py,sha256=HAd69yGTRL0zQsu-ywnbLu1UXiJzgHWuhYWA--vs4lQ,1181
|
28
28
|
pyplumio/helpers/timeout.py,sha256=JAhWNtIpcXyVILIwHWVy5mYofqbbRDGKLdTUKkQuajs,772
|
29
|
-
pyplumio/helpers/uid.py,sha256=
|
29
|
+
pyplumio/helpers/uid.py,sha256=qcE8sx8YwrUX3xEfL0cgjNP65rOZmv-M3fDlgFezUwc,989
|
30
30
|
pyplumio/structures/__init__.py,sha256=EjK-5qJZ0F7lpP2b6epvTMg9cIBl4Kn91nqNkEcLwTc,1299
|
31
31
|
pyplumio/structures/alerts.py,sha256=8ievMl5_tUBlnTLCiZoIloucIngCcoAYy6uI9sSXrt0,3664
|
32
32
|
pyplumio/structures/boiler_load.py,sha256=p3mOzZUU-g7A2tG_yp8podEqpI81hlsOZmHELyPNRY8,838
|
@@ -53,8 +53,8 @@ pyplumio/structures/statuses.py,sha256=wkoynyMRr1VREwfBC6vU48kPA8ZQ83pcXuciy2xHJ
|
|
53
53
|
pyplumio/structures/temperatures.py,sha256=1CDzehNmbALz1Jyt_9gZNIk52q6Wv-xQXjijVDCVYec,2337
|
54
54
|
pyplumio/structures/thermostat_parameters.py,sha256=QA-ZyulBG3P10sqgdI7rmpQYlKm9SJIXxBxAXs8Bwow,8295
|
55
55
|
pyplumio/structures/thermostat_sensors.py,sha256=8e1TxYIJTQKT0kIGO9gG4hGdLOBUpIhiPToQyOMyeNE,3237
|
56
|
-
PyPlumIO-0.5.
|
57
|
-
PyPlumIO-0.5.
|
58
|
-
PyPlumIO-0.5.
|
59
|
-
PyPlumIO-0.5.
|
60
|
-
PyPlumIO-0.5.
|
56
|
+
PyPlumIO-0.5.36.dist-info/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
57
|
+
PyPlumIO-0.5.36.dist-info/METADATA,sha256=F7biRxVKU0EPVYWi2uJXZYfVmvwM5j3C_o-O2aoJjQI,5510
|
58
|
+
PyPlumIO-0.5.36.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
59
|
+
PyPlumIO-0.5.36.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
60
|
+
PyPlumIO-0.5.36.dist-info/RECORD,,
|
pyplumio/__init__.py
CHANGED
pyplumio/_version.py
CHANGED
pyplumio/connection.py
CHANGED
@@ -86,7 +86,7 @@ class Connection(ABC, TaskManager):
|
|
86
86
|
RECONNECT_TIMEOUT,
|
87
87
|
)
|
88
88
|
await asyncio.sleep(RECONNECT_TIMEOUT)
|
89
|
-
self.create_task(self._reconnect())
|
89
|
+
self.create_task(self._reconnect(), name="reconnect_task")
|
90
90
|
|
91
91
|
async def connect(self) -> None:
|
92
92
|
"""Open the connection.
|
pyplumio/devices/__init__.py
CHANGED
@@ -13,7 +13,7 @@ from pyplumio.exceptions import RequestError, UnknownDeviceError
|
|
13
13
|
from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
|
14
14
|
from pyplumio.helpers.event_manager import EventManager
|
15
15
|
from pyplumio.helpers.factory import create_instance
|
16
|
-
from pyplumio.helpers.parameter import Parameter,
|
16
|
+
from pyplumio.helpers.parameter import NumericType, Parameter, State
|
17
17
|
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
|
18
18
|
from pyplumio.structures.network_info import NetworkInfo
|
19
19
|
from pyplumio.utils import to_camelcase
|
@@ -57,7 +57,7 @@ class Device(ABC, EventManager):
|
|
57
57
|
async def set(
|
58
58
|
self,
|
59
59
|
name: str,
|
60
|
-
value:
|
60
|
+
value: NumericType | State | bool,
|
61
61
|
retries: int = 5,
|
62
62
|
timeout: float | None = None,
|
63
63
|
) -> bool:
|
@@ -89,7 +89,7 @@ class Device(ABC, EventManager):
|
|
89
89
|
def set_nowait(
|
90
90
|
self,
|
91
91
|
name: str,
|
92
|
-
value:
|
92
|
+
value: NumericType | State | bool,
|
93
93
|
retries: int = 5,
|
94
94
|
timeout: float | None = None,
|
95
95
|
) -> None:
|
@@ -180,7 +180,7 @@ class PhysicalDevice(Device, ABC):
|
|
180
180
|
)
|
181
181
|
|
182
182
|
errors = [
|
183
|
-
result.
|
183
|
+
result.frame_type for result in results if isinstance(result, RequestError)
|
184
184
|
]
|
185
185
|
|
186
186
|
await asyncio.gather(
|
@@ -204,9 +204,9 @@ class PhysicalDevice(Device, ABC):
|
|
204
204
|
retries -= 1
|
205
205
|
|
206
206
|
raise RequestError(
|
207
|
-
f"Failed to request
|
208
|
-
f"
|
209
|
-
frame_type,
|
207
|
+
f"Failed to request '{name}' with frame type '{frame_type}' after "
|
208
|
+
f"{retries} retries.",
|
209
|
+
frame_type=frame_type,
|
210
210
|
)
|
211
211
|
|
212
212
|
@classmethod
|
pyplumio/devices/ecomax.py
CHANGED
@@ -59,7 +59,8 @@ ATTR_MIXERS: Final = "mixers"
|
|
59
59
|
ATTR_THERMOSTATS: Final = "thermostats"
|
60
60
|
ATTR_FUEL_BURNED: Final = "fuel_burned"
|
61
61
|
|
62
|
-
|
62
|
+
NANOSECONDS_IN_SECOND: Final = 1_000_000_000
|
63
|
+
MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * NANOSECONDS_IN_SECOND
|
63
64
|
|
64
65
|
SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
|
65
66
|
DataFrameDescription(
|
@@ -105,16 +106,15 @@ class EcoMAX(PhysicalDevice):
|
|
105
106
|
"""Represents an ecoMAX controller."""
|
106
107
|
|
107
108
|
address = DeviceType.ECOMAX
|
108
|
-
|
109
|
-
_fuel_burned_timestamp_ns: int
|
110
109
|
_setup_frames = SETUP_FRAME_TYPES
|
111
110
|
|
111
|
+
_fuel_burned_time_ns: int
|
112
|
+
|
112
113
|
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
113
114
|
"""Initialize a new ecoMAX controller."""
|
114
115
|
super().__init__(queue, network)
|
115
|
-
self._fuel_burned_timestamp_ns = time.perf_counter_ns()
|
116
116
|
self.subscribe(ATTR_ECOMAX_PARAMETERS, self._handle_ecomax_parameters)
|
117
|
-
self.subscribe(ATTR_FUEL_CONSUMPTION, self.
|
117
|
+
self.subscribe(ATTR_FUEL_CONSUMPTION, self._add_burned_fuel_meter)
|
118
118
|
self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
|
119
119
|
self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
|
120
120
|
self.subscribe(ATTR_SCHEDULES, self._add_schedules)
|
@@ -124,6 +124,7 @@ class EcoMAX(PhysicalDevice):
|
|
124
124
|
self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_thermostat_parameters)
|
125
125
|
self.subscribe(ATTR_THERMOSTAT_PROFILE, self._add_thermostat_profile_parameter)
|
126
126
|
self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
|
127
|
+
self._fuel_burned_time_ns = time.perf_counter_ns()
|
127
128
|
|
128
129
|
async def async_setup(self) -> bool:
|
129
130
|
"""Set up an ecoMAX controller."""
|
@@ -183,13 +184,10 @@ class EcoMAX(PhysicalDevice):
|
|
183
184
|
description = ECOMAX_PARAMETERS[product.type][index]
|
184
185
|
except IndexError:
|
185
186
|
_LOGGER.warning(
|
186
|
-
(
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
"Please visit the issue tracker and open a feature "
|
191
|
-
"request to support %s"
|
192
|
-
),
|
187
|
+
"Encountered unknown ecoMAX parameter (%i): %s. "
|
188
|
+
"Your device isn't fully compatible with this software "
|
189
|
+
"and may not work properly. Please visit the issue tracker "
|
190
|
+
"and open a feature request to support %s",
|
193
191
|
index,
|
194
192
|
values,
|
195
193
|
product.model,
|
@@ -211,19 +209,29 @@ class EcoMAX(PhysicalDevice):
|
|
211
209
|
await asyncio.gather(*_ecomax_parameter_events())
|
212
210
|
return True
|
213
211
|
|
214
|
-
async def
|
215
|
-
"""Calculate
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
212
|
+
async def _add_burned_fuel_meter(self, fuel_consumption: float) -> None:
|
213
|
+
"""Calculate and dispatch the amount of fuel burned.
|
214
|
+
|
215
|
+
This method calculates the fuel burned based on the time
|
216
|
+
elapsed since the last sensor message, which contains fuel
|
217
|
+
consumption data. If the elapsed time is within the acceptable
|
218
|
+
range, it dispatches the fuel burned data. Otherwise, it logs a
|
219
|
+
warning and skips the outdated data.
|
220
|
+
"""
|
221
|
+
time_ns = time.perf_counter_ns()
|
222
|
+
nanoseconds_passed = time_ns - self._fuel_burned_time_ns
|
223
|
+
self._fuel_burned_time_ns = time_ns
|
224
|
+
if nanoseconds_passed < MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
|
225
|
+
return self.dispatch_nowait(
|
226
|
+
ATTR_FUEL_BURNED,
|
227
|
+
fuel_consumption * nanoseconds_passed / (3600 * NANOSECONDS_IN_SECOND),
|
223
228
|
)
|
224
|
-
|
225
|
-
|
226
|
-
|
229
|
+
|
230
|
+
_LOGGER.warning(
|
231
|
+
"Skipping outdated fuel consumption data: %f (was %i seconds old)",
|
232
|
+
fuel_consumption,
|
233
|
+
nanoseconds_passed / NANOSECONDS_IN_SECOND,
|
234
|
+
)
|
227
235
|
|
228
236
|
async def _handle_mixer_parameters(
|
229
237
|
self,
|
pyplumio/devices/mixer.py
CHANGED
@@ -64,13 +64,10 @@ class Mixer(VirtualDevice):
|
|
64
64
|
description = MIXER_PARAMETERS[product.type][index]
|
65
65
|
except IndexError:
|
66
66
|
_LOGGER.warning(
|
67
|
-
(
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
"Please visit the issue tracker and open a feature "
|
72
|
-
"request to support %s"
|
73
|
-
),
|
67
|
+
"Encountered unknown mixer parameter (%i): %s. "
|
68
|
+
"Your device isn't fully compatible with this software "
|
69
|
+
"and may not work properly. Please visit the issue tracker "
|
70
|
+
"and open a feature request to support %s",
|
74
71
|
index,
|
75
72
|
values,
|
76
73
|
product.model,
|
pyplumio/exceptions.py
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
from pyplumio.const import FrameType
|
6
|
+
|
5
7
|
|
6
8
|
class PyPlumIOError(Exception):
|
7
9
|
"""Base PyPlumIO error class."""
|
@@ -11,13 +13,18 @@ class ConnectionFailedError(PyPlumIOError):
|
|
11
13
|
"""Raised on connection failure."""
|
12
14
|
|
13
15
|
|
14
|
-
class ProtocolError(PyPlumIOError):
|
15
|
-
"""Base class for protocol-related errors."""
|
16
|
-
|
17
|
-
|
18
16
|
class RequestError(PyPlumIOError):
|
19
17
|
"""Raised on request error."""
|
20
18
|
|
19
|
+
def __init__(self, message: str, frame_type: FrameType) -> None:
|
20
|
+
"""Initialize a new RequestError."""
|
21
|
+
super().__init__(message)
|
22
|
+
self.frame_type = frame_type
|
23
|
+
|
24
|
+
|
25
|
+
class ProtocolError(PyPlumIOError):
|
26
|
+
"""Base class for protocol-related errors."""
|
27
|
+
|
21
28
|
|
22
29
|
class ReadError(ProtocolError):
|
23
30
|
"""Raised on read error."""
|
pyplumio/frames/requests.py
CHANGED
@@ -20,16 +20,20 @@ from pyplumio.frames.responses import DeviceAvailableResponse, ProgramVersionRes
|
|
20
20
|
from pyplumio.structures.schedules import SchedulesStructure
|
21
21
|
|
22
22
|
|
23
|
-
class
|
24
|
-
"""Represents
|
23
|
+
class AlertsRequest(Request):
|
24
|
+
"""Represents an alerts request.
|
25
|
+
|
26
|
+
Contains number of alerts to get and index of the first
|
27
|
+
alert.
|
28
|
+
"""
|
25
29
|
|
26
30
|
__slots__ = ()
|
27
31
|
|
28
|
-
frame_type = FrameType.
|
32
|
+
frame_type = FrameType.REQUEST_ALERTS
|
29
33
|
|
30
|
-
def
|
31
|
-
"""
|
32
|
-
return
|
34
|
+
def create_message(self, data: dict[str, Any]) -> bytearray:
|
35
|
+
"""Create a frame message."""
|
36
|
+
return bytearray([data.get(ATTR_START, 0), data.get(ATTR_COUNT, 10)])
|
33
37
|
|
34
38
|
|
35
39
|
class CheckDeviceRequest(Request):
|
@@ -44,20 +48,23 @@ class CheckDeviceRequest(Request):
|
|
44
48
|
return DeviceAvailableResponse(recipient=self.sender, **kwargs)
|
45
49
|
|
46
50
|
|
47
|
-
class
|
48
|
-
"""Represents an
|
49
|
-
|
50
|
-
__slots__ = ()
|
51
|
-
|
52
|
-
frame_type = FrameType.REQUEST_UID
|
53
|
-
|
51
|
+
class EcomaxControlRequest(Request):
|
52
|
+
"""Represents an ecoMAX control request.
|
54
53
|
|
55
|
-
|
56
|
-
|
54
|
+
Contains single binary value. 0 - means that controller should
|
55
|
+
be turned off, 1 - means that it should be turned on.
|
56
|
+
"""
|
57
57
|
|
58
58
|
__slots__ = ()
|
59
59
|
|
60
|
-
frame_type = FrameType.
|
60
|
+
frame_type = FrameType.REQUEST_ECOMAX_CONTROL
|
61
|
+
|
62
|
+
def create_message(self, data: dict[str, Any]) -> bytearray:
|
63
|
+
"""Create a frame message."""
|
64
|
+
try:
|
65
|
+
return bytearray([data[ATTR_VALUE]])
|
66
|
+
except KeyError as e:
|
67
|
+
raise FrameDataError from e
|
61
68
|
|
62
69
|
|
63
70
|
class EcomaxParametersRequest(Request):
|
@@ -91,20 +98,24 @@ class MixerParametersRequest(Request):
|
|
91
98
|
return bytearray([data.get(ATTR_COUNT, 255), data.get(ATTR_START, 0)])
|
92
99
|
|
93
100
|
|
94
|
-
class
|
95
|
-
"""Represents a
|
101
|
+
class PasswordRequest(Request):
|
102
|
+
"""Represents a password request."""
|
96
103
|
|
97
|
-
|
98
|
-
|
99
|
-
|
104
|
+
__slots__ = ()
|
105
|
+
|
106
|
+
frame_type = FrameType.REQUEST_PASSWORD
|
107
|
+
|
108
|
+
|
109
|
+
class ProgramVersionRequest(Request):
|
110
|
+
"""Represents a program version request."""
|
100
111
|
|
101
112
|
__slots__ = ()
|
102
113
|
|
103
|
-
frame_type = FrameType.
|
114
|
+
frame_type = FrameType.REQUEST_PROGRAM_VERSION
|
104
115
|
|
105
|
-
def
|
106
|
-
"""
|
107
|
-
return
|
116
|
+
def response(self, **kwargs: Any) -> Response | None:
|
117
|
+
"""Return a response frame."""
|
118
|
+
return ProgramVersionResponse(recipient=self.sender, **kwargs)
|
108
119
|
|
109
120
|
|
110
121
|
class RegulatorDataSchemaRequest(Request):
|
@@ -115,6 +126,14 @@ class RegulatorDataSchemaRequest(Request):
|
|
115
126
|
frame_type = FrameType.REQUEST_REGULATOR_DATA_SCHEMA
|
116
127
|
|
117
128
|
|
129
|
+
class SchedulesRequest(Request):
|
130
|
+
"""Represents a schedules request."""
|
131
|
+
|
132
|
+
__slots__ = ()
|
133
|
+
|
134
|
+
frame_type = FrameType.REQUEST_SCHEDULES
|
135
|
+
|
136
|
+
|
118
137
|
class SetEcomaxParameterRequest(Request):
|
119
138
|
"""Represents a request to set an ecoMAX parameter.
|
120
139
|
|
@@ -153,6 +172,18 @@ class SetMixerParameterRequest(Request):
|
|
153
172
|
raise FrameDataError from e
|
154
173
|
|
155
174
|
|
175
|
+
class SetScheduleRequest(Request):
|
176
|
+
"""Represents a request to set a schedule."""
|
177
|
+
|
178
|
+
__slots__ = ()
|
179
|
+
|
180
|
+
frame_type = FrameType.REQUEST_SET_SCHEDULE
|
181
|
+
|
182
|
+
def create_message(self, data: dict[str, Any]) -> bytearray:
|
183
|
+
"""Create a frame message."""
|
184
|
+
return SchedulesStructure(self).encode(data)
|
185
|
+
|
186
|
+
|
156
187
|
class SetThermostatParameterRequest(Request):
|
157
188
|
"""Represents a request to set a thermostat parameter.
|
158
189
|
|
@@ -183,25 +214,6 @@ class SetThermostatParameterRequest(Request):
|
|
183
214
|
raise FrameDataError from e
|
184
215
|
|
185
216
|
|
186
|
-
class EcomaxControlRequest(Request):
|
187
|
-
"""Represents an ecoMAX control request.
|
188
|
-
|
189
|
-
Contains single binary value. 0 - means that controller should
|
190
|
-
be turned off, 1 - means that it should be turned on.
|
191
|
-
"""
|
192
|
-
|
193
|
-
__slots__ = ()
|
194
|
-
|
195
|
-
frame_type = FrameType.REQUEST_ECOMAX_CONTROL
|
196
|
-
|
197
|
-
def create_message(self, data: dict[str, Any]) -> bytearray:
|
198
|
-
"""Create a frame message."""
|
199
|
-
try:
|
200
|
-
return bytearray([data[ATTR_VALUE]])
|
201
|
-
except KeyError as e:
|
202
|
-
raise FrameDataError from e
|
203
|
-
|
204
|
-
|
205
217
|
class StartMasterRequest(Request):
|
206
218
|
"""Represents a request to become a master.
|
207
219
|
|
@@ -226,37 +238,25 @@ class StopMasterRequest(Request):
|
|
226
238
|
frame_type = FrameType.REQUEST_STOP_MASTER
|
227
239
|
|
228
240
|
|
229
|
-
class
|
230
|
-
"""Represents
|
241
|
+
class ThermostatParametersRequest(Request):
|
242
|
+
"""Represents a thermostat parameters request.
|
231
243
|
|
232
|
-
Contains number of
|
233
|
-
|
244
|
+
Contains number of parameters to get and index of the first
|
245
|
+
parameter.
|
234
246
|
"""
|
235
247
|
|
236
248
|
__slots__ = ()
|
237
249
|
|
238
|
-
frame_type = FrameType.
|
250
|
+
frame_type = FrameType.REQUEST_THERMOSTAT_PARAMETERS
|
239
251
|
|
240
252
|
def create_message(self, data: dict[str, Any]) -> bytearray:
|
241
253
|
"""Create a frame message."""
|
242
|
-
return bytearray([data.get(
|
243
|
-
|
244
|
-
|
245
|
-
class SchedulesRequest(Request):
|
246
|
-
"""Represents a schedules request."""
|
247
|
-
|
248
|
-
__slots__ = ()
|
249
|
-
|
250
|
-
frame_type = FrameType.REQUEST_SCHEDULES
|
254
|
+
return bytearray([data.get(ATTR_COUNT, 255), data.get(ATTR_START, 0)])
|
251
255
|
|
252
256
|
|
253
|
-
class
|
254
|
-
"""Represents
|
257
|
+
class UIDRequest(Request):
|
258
|
+
"""Represents an UID request."""
|
255
259
|
|
256
260
|
__slots__ = ()
|
257
261
|
|
258
|
-
frame_type = FrameType.
|
259
|
-
|
260
|
-
def create_message(self, data: dict[str, Any]) -> bytearray:
|
261
|
-
"""Create a frame message."""
|
262
|
-
return SchedulesStructure(self).encode(data)
|
262
|
+
frame_type = FrameType.REQUEST_UID
|
pyplumio/frames/responses.py
CHANGED
@@ -17,23 +17,16 @@ from pyplumio.structures.schedules import SchedulesStructure
|
|
17
17
|
from pyplumio.structures.thermostat_parameters import ThermostatParametersStructure
|
18
18
|
|
19
19
|
|
20
|
-
class
|
21
|
-
"""Represents a
|
22
|
-
|
23
|
-
Contains software version info.
|
24
|
-
"""
|
20
|
+
class AlertsResponse(Response):
|
21
|
+
"""Represents response to a device alerts request."""
|
25
22
|
|
26
23
|
__slots__ = ()
|
27
24
|
|
28
|
-
frame_type = FrameType.
|
29
|
-
|
30
|
-
def create_message(self, data: dict[str, Any]) -> bytearray:
|
31
|
-
"""Create a frame message."""
|
32
|
-
return ProgramVersionStructure(self).encode(data)
|
25
|
+
frame_type = FrameType.RESPONSE_ALERTS
|
33
26
|
|
34
27
|
def decode_message(self, message: bytearray) -> dict[str, Any]:
|
35
28
|
"""Decode a frame message."""
|
36
|
-
return
|
29
|
+
return AlertsStructure(self).decode(message)[0]
|
37
30
|
|
38
31
|
|
39
32
|
class DeviceAvailableResponse(Response):
|
@@ -55,39 +48,16 @@ class DeviceAvailableResponse(Response):
|
|
55
48
|
return NetworkInfoStructure(self).decode(message, offset=1)[0]
|
56
49
|
|
57
50
|
|
58
|
-
class
|
59
|
-
"""Represents an
|
60
|
-
|
61
|
-
Contains product info and product UID.
|
62
|
-
"""
|
63
|
-
|
64
|
-
__slots__ = ()
|
65
|
-
|
66
|
-
frame_type = FrameType.RESPONSE_UID
|
67
|
-
|
68
|
-
def create_message(self, data: dict[str, Any]) -> bytearray:
|
69
|
-
"""Create a frame message."""
|
70
|
-
return ProductInfoStructure(self).encode(data)
|
71
|
-
|
72
|
-
def decode_message(self, message: bytearray) -> dict[str, Any]:
|
73
|
-
"""Decode a frame message."""
|
74
|
-
return ProductInfoStructure(self).decode(message)[0]
|
75
|
-
|
76
|
-
|
77
|
-
class PasswordResponse(Response):
|
78
|
-
"""Represents a password response.
|
51
|
+
class EcomaxControlResponse(Response):
|
52
|
+
"""Represents response to an ecoMAX control request.
|
79
53
|
|
80
|
-
|
54
|
+
Empty response acknowledges, that ecoMAX control request was
|
55
|
+
successfully processed.
|
81
56
|
"""
|
82
57
|
|
83
58
|
__slots__ = ()
|
84
59
|
|
85
|
-
frame_type = FrameType.
|
86
|
-
|
87
|
-
def decode_message(self, message: bytearray) -> dict[str, Any]:
|
88
|
-
"""Decode a frame message."""
|
89
|
-
password = message[1:].decode() if message[1:] else None
|
90
|
-
return {ATTR_PASSWORD: password}
|
60
|
+
frame_type = FrameType.RESPONSE_ECOMAX_CONTROL
|
91
61
|
|
92
62
|
|
93
63
|
class EcomaxParametersResponse(Response):
|
@@ -120,19 +90,39 @@ class MixerParametersResponse(Response):
|
|
120
90
|
return MixerParametersStructure(self).decode(message)[0]
|
121
91
|
|
122
92
|
|
123
|
-
class
|
124
|
-
"""Represents a
|
93
|
+
class PasswordResponse(Response):
|
94
|
+
"""Represents a password response.
|
125
95
|
|
126
|
-
Contains
|
96
|
+
Contains device service password as plaintext.
|
127
97
|
"""
|
128
98
|
|
129
99
|
__slots__ = ()
|
130
100
|
|
131
|
-
frame_type = FrameType.
|
101
|
+
frame_type = FrameType.RESPONSE_PASSWORD
|
132
102
|
|
133
103
|
def decode_message(self, message: bytearray) -> dict[str, Any]:
|
134
104
|
"""Decode a frame message."""
|
135
|
-
|
105
|
+
password = message[1:].decode() if message[1:] else None
|
106
|
+
return {ATTR_PASSWORD: password}
|
107
|
+
|
108
|
+
|
109
|
+
class ProgramVersionResponse(Response):
|
110
|
+
"""Represents a program version response.
|
111
|
+
|
112
|
+
Contains software version info.
|
113
|
+
"""
|
114
|
+
|
115
|
+
__slots__ = ()
|
116
|
+
|
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]
|
136
126
|
|
137
127
|
|
138
128
|
class RegulatorDataSchemaResponse(Response):
|
@@ -151,6 +141,18 @@ class RegulatorDataSchemaResponse(Response):
|
|
151
141
|
return RegulatorDataSchemaStructure(self).decode(message)[0]
|
152
142
|
|
153
143
|
|
144
|
+
class SchedulesResponse(Response):
|
145
|
+
"""Represents response to a device schedules request."""
|
146
|
+
|
147
|
+
__slots__ = ()
|
148
|
+
|
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
|
+
|
154
156
|
class SetEcomaxParameterResponse(Response):
|
155
157
|
"""Represents response to a set ecoMAX parameter request.
|
156
158
|
|
@@ -187,37 +189,35 @@ class SetThermostatParameterResponse(Response):
|
|
187
189
|
frame_type = FrameType.RESPONSE_SET_THERMOSTAT_PARAMETER
|
188
190
|
|
189
191
|
|
190
|
-
class
|
191
|
-
"""Represents
|
192
|
+
class ThermostatParametersResponse(Response):
|
193
|
+
"""Represents a thermostat parameters response.
|
192
194
|
|
193
|
-
|
194
|
-
successfully processed.
|
195
|
+
Contains editable thermostat parameters.
|
195
196
|
"""
|
196
197
|
|
197
198
|
__slots__ = ()
|
198
199
|
|
199
|
-
frame_type = FrameType.
|
200
|
-
|
201
|
-
|
202
|
-
class AlertsResponse(Response):
|
203
|
-
"""Represents response to a device alerts request."""
|
204
|
-
|
205
|
-
__slots__ = ()
|
206
|
-
|
207
|
-
frame_type = FrameType.RESPONSE_ALERTS
|
200
|
+
frame_type = FrameType.RESPONSE_THERMOSTAT_PARAMETERS
|
208
201
|
|
209
202
|
def decode_message(self, message: bytearray) -> dict[str, Any]:
|
210
203
|
"""Decode a frame message."""
|
211
|
-
return
|
204
|
+
return ThermostatParametersStructure(self).decode(message)[0]
|
212
205
|
|
213
206
|
|
214
|
-
class
|
215
|
-
"""Represents
|
207
|
+
class UIDResponse(Response):
|
208
|
+
"""Represents an UID response.
|
209
|
+
|
210
|
+
Contains product info and product UID.
|
211
|
+
"""
|
216
212
|
|
217
213
|
__slots__ = ()
|
218
214
|
|
219
|
-
frame_type = FrameType.
|
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
220
|
|
221
221
|
def decode_message(self, message: bytearray) -> dict[str, Any]:
|
222
222
|
"""Decode a frame message."""
|
223
|
-
return
|
223
|
+
return ProductInfoStructure(self).decode(message)[0]
|
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, Literal, TypeVar, Union
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
|
10
10
|
|
11
11
|
from dataslots import dataslots
|
12
12
|
from typing_extensions import TypeAlias
|
@@ -19,8 +19,8 @@ if TYPE_CHECKING:
|
|
19
19
|
|
20
20
|
_LOGGER = logging.getLogger(__name__)
|
21
21
|
|
22
|
-
|
23
|
-
|
22
|
+
NumericType: TypeAlias = Union[int, float]
|
23
|
+
State: TypeAlias = Literal["on", "off"]
|
24
24
|
ParameterT = TypeVar("ParameterT", bound="Parameter")
|
25
25
|
|
26
26
|
|
@@ -28,7 +28,7 @@ def unpack_parameter(
|
|
28
28
|
data: bytearray, offset: int = 0, size: int = 1
|
29
29
|
) -> ParameterValues | None:
|
30
30
|
"""Unpack a device parameter."""
|
31
|
-
if not
|
31
|
+
if not validate_parameter(data[offset : offset + size * 3]):
|
32
32
|
return None
|
33
33
|
|
34
34
|
value = data[offset : offset + size]
|
@@ -42,14 +42,17 @@ def unpack_parameter(
|
|
42
42
|
)
|
43
43
|
|
44
44
|
|
45
|
-
def
|
45
|
+
def validate_parameter(data: bytearray) -> bool:
|
46
46
|
"""Check if parameter contains any bytes besides 0xFF."""
|
47
47
|
return any(x for x in data if x != BYTE_UNDEFINED)
|
48
48
|
|
49
49
|
|
50
|
-
def
|
51
|
-
"""
|
52
|
-
|
50
|
+
def parameter_value_to_int(value: NumericType | State | bool) -> int:
|
51
|
+
"""Convert a parameter value to an integer.
|
52
|
+
|
53
|
+
If the value is STATE_OFF or STATE_ON, it returns 0 or 1 respectively.
|
54
|
+
"""
|
55
|
+
if value in get_args(State):
|
53
56
|
return 1 if value == STATE_ON else 0
|
54
57
|
|
55
58
|
return int(value)
|
@@ -77,19 +80,11 @@ class ParameterDescription:
|
|
77
80
|
class Parameter(ABC):
|
78
81
|
"""Represents a base parameter."""
|
79
82
|
|
80
|
-
__slots__ = (
|
81
|
-
"device",
|
82
|
-
"description",
|
83
|
-
"_pending_update",
|
84
|
-
"_previous_value",
|
85
|
-
"_index",
|
86
|
-
"_values",
|
87
|
-
)
|
83
|
+
__slots__ = ("device", "description", "_pending_update", "_index", "_values")
|
88
84
|
|
89
85
|
device: Device
|
90
86
|
description: ParameterDescription
|
91
87
|
_pending_update: bool
|
92
|
-
_previous_value: int
|
93
88
|
_index: int
|
94
89
|
_values: ParameterValues
|
95
90
|
|
@@ -103,8 +98,8 @@ class Parameter(ABC):
|
|
103
98
|
"""Initialize a new parameter."""
|
104
99
|
self.device = device
|
105
100
|
self.description = description
|
101
|
+
self._index = index
|
106
102
|
self._pending_update = False
|
107
|
-
self._previous_value = 0
|
108
103
|
self._index = index
|
109
104
|
self._values = values if values else ParameterValues(0, 0, 0)
|
110
105
|
|
@@ -127,9 +122,9 @@ class Parameter(ABC):
|
|
127
122
|
handler = getattr(self.values, method_to_call)
|
128
123
|
return handler(other)
|
129
124
|
|
130
|
-
if isinstance(other, (int, float, bool)) or other in (
|
125
|
+
if isinstance(other, (int, float, bool)) or other in get_args(State):
|
131
126
|
handler = getattr(self.values.value, method_to_call)
|
132
|
-
return handler(
|
127
|
+
return handler(parameter_value_to_int(other))
|
133
128
|
else:
|
134
129
|
return NotImplemented
|
135
130
|
|
@@ -184,42 +179,44 @@ class Parameter(ABC):
|
|
184
179
|
)
|
185
180
|
return type(self)(self.device, self.description, values)
|
186
181
|
|
187
|
-
def validate(self, value:
|
182
|
+
def validate(self, value: NumericType | State | bool) -> int:
|
188
183
|
"""Validate a parameter value."""
|
189
|
-
|
190
|
-
if
|
184
|
+
int_value = parameter_value_to_int(value)
|
185
|
+
if int_value < self.values.min_value or int_value > self.values.max_value:
|
191
186
|
raise ValueError(
|
192
187
|
f"Invalid value: {value}. Must be between "
|
193
188
|
f"{self.min_value} and {self.max_value}."
|
194
189
|
)
|
195
190
|
|
196
|
-
return
|
191
|
+
return int_value
|
197
192
|
|
198
193
|
async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
|
199
194
|
"""Set a parameter value."""
|
200
|
-
return await self.
|
195
|
+
return await self._attempt_update(self.validate(value), retries, timeout)
|
201
196
|
|
202
197
|
def set_nowait(self, value: Any, retries: int = 5, timeout: float = 5.0) -> None:
|
203
198
|
"""Set a parameter value without waiting."""
|
204
|
-
self.device.create_task(
|
199
|
+
self.device.create_task(
|
200
|
+
self._attempt_update(self.validate(value), retries, timeout)
|
201
|
+
)
|
205
202
|
|
206
|
-
async def
|
207
|
-
self, value:
|
203
|
+
async def _attempt_update(
|
204
|
+
self, value: int, retries: int = 5, timeout: float = 5.0
|
208
205
|
) -> bool:
|
209
|
-
"""
|
206
|
+
"""Attempt to update a parameter value on the remote device."""
|
210
207
|
if value == self.values.value:
|
211
208
|
# Value is unchanged
|
212
209
|
return True
|
213
210
|
|
214
|
-
self._previous_value = self._values.value
|
215
|
-
self._values.value = value
|
216
211
|
self._pending_update = True
|
212
|
+
self._values.value = value
|
213
|
+
initial_retries = retries
|
217
214
|
while self.pending_update:
|
218
215
|
if retries <= 0:
|
219
216
|
_LOGGER.warning(
|
220
|
-
"Unable to confirm
|
217
|
+
"Unable to confirm update of '%s' parameter after %d retries",
|
221
218
|
self.description.name,
|
222
|
-
|
219
|
+
initial_retries,
|
223
220
|
)
|
224
221
|
return False
|
225
222
|
|
@@ -231,9 +228,7 @@ class Parameter(ABC):
|
|
231
228
|
|
232
229
|
def update(self, values: ParameterValues) -> None:
|
233
230
|
"""Update the parameter values."""
|
234
|
-
|
235
|
-
self._pending_update = False
|
236
|
-
|
231
|
+
self._pending_update = False
|
237
232
|
self._values = values
|
238
233
|
|
239
234
|
@property
|
@@ -267,17 +262,17 @@ class Parameter(ABC):
|
|
267
262
|
|
268
263
|
@property
|
269
264
|
@abstractmethod
|
270
|
-
def value(self) ->
|
265
|
+
def value(self) -> NumericType | State | bool:
|
271
266
|
"""Return the value."""
|
272
267
|
|
273
268
|
@property
|
274
269
|
@abstractmethod
|
275
|
-
def min_value(self) ->
|
270
|
+
def min_value(self) -> NumericType | State | bool:
|
276
271
|
"""Return the minimum allowed value."""
|
277
272
|
|
278
273
|
@property
|
279
274
|
@abstractmethod
|
280
|
-
def max_value(self) ->
|
275
|
+
def max_value(self) -> NumericType | State | bool:
|
281
276
|
"""Return the maximum allowed value."""
|
282
277
|
|
283
278
|
@abstractmethod
|
@@ -301,13 +296,13 @@ class Number(Parameter):
|
|
301
296
|
description: NumberDescription
|
302
297
|
|
303
298
|
async def set(
|
304
|
-
self, value:
|
299
|
+
self, value: NumericType, retries: int = 5, timeout: float = 5.0
|
305
300
|
) -> bool:
|
306
301
|
"""Set a parameter value."""
|
307
302
|
return await super().set(value, retries, timeout)
|
308
303
|
|
309
304
|
def set_nowait(
|
310
|
-
self, value:
|
305
|
+
self, value: NumericType, retries: int = 5, timeout: float = 5.0
|
311
306
|
) -> None:
|
312
307
|
"""Set a parameter value without waiting."""
|
313
308
|
super().set_nowait(value, retries, timeout)
|
@@ -317,17 +312,17 @@ class Number(Parameter):
|
|
317
312
|
return Request()
|
318
313
|
|
319
314
|
@property
|
320
|
-
def value(self) ->
|
315
|
+
def value(self) -> NumericType:
|
321
316
|
"""Return the value."""
|
322
317
|
return self.values.value
|
323
318
|
|
324
319
|
@property
|
325
|
-
def min_value(self) ->
|
320
|
+
def min_value(self) -> NumericType:
|
326
321
|
"""Return the minimum allowed value."""
|
327
322
|
return self.values.min_value
|
328
323
|
|
329
324
|
@property
|
330
|
-
def max_value(self) ->
|
325
|
+
def max_value(self) -> NumericType:
|
331
326
|
"""Return the maximum allowed value."""
|
332
327
|
return self.values.max_value
|
333
328
|
|
@@ -351,13 +346,13 @@ class Switch(Parameter):
|
|
351
346
|
description: SwitchDescription
|
352
347
|
|
353
348
|
async def set(
|
354
|
-
self, value:
|
349
|
+
self, value: State | bool, retries: int = 5, timeout: float = 5.0
|
355
350
|
) -> bool:
|
356
351
|
"""Set a parameter value."""
|
357
352
|
return await super().set(value, retries, timeout)
|
358
353
|
|
359
354
|
def set_nowait(
|
360
|
-
self, value:
|
355
|
+
self, value: State | bool, retries: int = 5, timeout: float = 5.0
|
361
356
|
) -> None:
|
362
357
|
"""Set a switch value without waiting."""
|
363
358
|
super().set_nowait(value, retries, timeout)
|
@@ -393,7 +388,7 @@ class Switch(Parameter):
|
|
393
388
|
return Request()
|
394
389
|
|
395
390
|
@property
|
396
|
-
def value(self) ->
|
391
|
+
def value(self) -> State:
|
397
392
|
"""Return the value."""
|
398
393
|
return STATE_ON if self.values.value == 1 else STATE_OFF
|
399
394
|
|
pyplumio/helpers/schedule.py
CHANGED
@@ -21,8 +21,7 @@ TIME_FORMAT: Final = "%H:%M"
|
|
21
21
|
STATE_NIGHT: Final = "night"
|
22
22
|
STATE_DAY: Final = "day"
|
23
23
|
|
24
|
-
|
25
|
-
OFF_STATES: Final = (STATE_OFF, STATE_NIGHT)
|
24
|
+
_ON_STATES: Final = {STATE_ON, STATE_DAY}
|
26
25
|
|
27
26
|
ScheduleState: TypeAlias = Literal["on", "off", "day", "night"]
|
28
27
|
Time = Annotated[str, "time in HH:MM format"]
|
@@ -113,7 +112,7 @@ class ScheduleDay(MutableMapping):
|
|
113
112
|
)
|
114
113
|
|
115
114
|
for index in _get_time_range(start, end):
|
116
|
-
self._intervals[index] = True if state in
|
115
|
+
self._intervals[index] = True if state in _ON_STATES else False
|
117
116
|
|
118
117
|
def set_on(self, start: Time = "00:00", end: Time = "00:00") -> None:
|
119
118
|
"""Set a schedule interval state to 'on'."""
|
pyplumio/helpers/uid.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""Contains
|
1
|
+
"""Contains UID helpers."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
@@ -18,18 +18,18 @@ def decode_uid(buffer: bytes) -> str:
|
|
18
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
|
-
output =
|
21
|
+
output = []
|
22
22
|
while number:
|
23
|
-
output
|
23
|
+
output.append(BASE5_KEY[number & 0b00011111])
|
24
24
|
number >>= 5
|
25
25
|
|
26
|
-
return output
|
26
|
+
return "".join(reversed(output))
|
27
27
|
|
28
28
|
|
29
29
|
def _crc16(buffer: bytes) -> bytes:
|
30
30
|
"""Return a CRC 16."""
|
31
31
|
crc16 = reduce(_crc16_byte, buffer, CRC)
|
32
|
-
return crc16.to_bytes(byteorder="little"
|
32
|
+
return crc16.to_bytes(length=2, byteorder="little")
|
33
33
|
|
34
34
|
|
35
35
|
def _crc16_byte(crc: int, byte: int) -> int:
|
File without changes
|
File without changes
|
File without changes
|