PyPlumIO 0.5.35__py3-none-any.whl → 0.5.37__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.37.dist-info}/METADATA +9 -9
- {PyPlumIO-0.5.35.dist-info → PyPlumIO-0.5.37.dist-info}/RECORD +18 -18
- 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 +51 -47
- pyplumio/helpers/schedule.py +2 -3
- pyplumio/helpers/uid.py +5 -5
- pyplumio/structures/ecomax_parameters.py +3 -1
- {PyPlumIO-0.5.35.dist-info → PyPlumIO-0.5.37.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.35.dist-info → PyPlumIO-0.5.37.dist-info}/WHEEL +0 -0
- {PyPlumIO-0.5.35.dist-info → PyPlumIO-0.5.37.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.37
|
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,30 +27,30 @@ 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.1; 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
|
-
Requires-Dist: pytest-asyncio==0.25.
|
36
|
-
Requires-Dist: ruff==0.9.
|
37
|
-
Requires-Dist: tox==4.
|
38
|
-
Requires-Dist: types-pyserial==3.5.0.
|
35
|
+
Requires-Dist: pytest-asyncio==0.25.3; extra == "test"
|
36
|
+
Requires-Dist: ruff==0.9.4; extra == "test"
|
37
|
+
Requires-Dist: tox==4.24.1; extra == "test"
|
38
|
+
Requires-Dist: types-pyserial==3.5.0.20250130; 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.
|
49
49
|
[](https://badge.fury.io/py/PyPlumIO)
|
50
50
|
[](https://pypi.python.org/pypi/pyplumio/)
|
51
51
|
[](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml)
|
52
|
-
[](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
|
53
|
+
[](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
|
54
54
|
[](https://guidelines.denpa.pro/stability#release-candidate)
|
55
55
|
[](https://github.com/astral-sh/ruff)
|
56
56
|
|
@@ -1,37 +1,37 @@
|
|
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=orZLgglRSt7G3Yf4YysqzFGUuXQI2XkO9JT2vDRdeJc,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=LoTYAtSLv2bGjEMABn7S1Ycqd_DzcMt_6UPG8frFZ-8,12740
|
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
|
33
33
|
pyplumio/structures/boiler_power.py,sha256=72qsvccg49FdRdXv2f2K5sGpjT7wAOLFjlIGWpO-DVg,901
|
34
|
-
pyplumio/structures/ecomax_parameters.py,sha256=
|
34
|
+
pyplumio/structures/ecomax_parameters.py,sha256=4hsLM8pgcLrfYL0loLqTH4kMSdVzOThu5SL_QTodSYs,27997
|
35
35
|
pyplumio/structures/fan_power.py,sha256=Q5fv-7_2NVuLeQPIVIylvgN7M8-a9D8rRUE0QGjyS3w,871
|
36
36
|
pyplumio/structures/frame_versions.py,sha256=hbcVuhuPNy5qd39Vk7w4WdPCW-TNx1cAYWzA2mXocyk,1548
|
37
37
|
pyplumio/structures/fuel_consumption.py,sha256=_p2dI4H67Eopn7IF0Gj77A8c_8lNKhhDDAtmugxLd4s,976
|
@@ -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.37.dist-info/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
57
|
+
PyPlumIO-0.5.37.dist-info/METADATA,sha256=CNXGeqKEbJVnM1LlpzV33S7TDJXCJYlrY8pU8n_4w1w,5510
|
58
|
+
PyPlumIO-0.5.37.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
59
|
+
PyPlumIO-0.5.37.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
60
|
+
PyPlumIO-0.5.37.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 is_valid_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 is_valid_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)
|
@@ -72,24 +75,17 @@ class ParameterDescription:
|
|
72
75
|
"""Represents a parameter description."""
|
73
76
|
|
74
77
|
name: str
|
78
|
+
optimistic: bool = False
|
75
79
|
|
76
80
|
|
77
81
|
class Parameter(ABC):
|
78
82
|
"""Represents a base parameter."""
|
79
83
|
|
80
|
-
__slots__ = (
|
81
|
-
"device",
|
82
|
-
"description",
|
83
|
-
"_pending_update",
|
84
|
-
"_previous_value",
|
85
|
-
"_index",
|
86
|
-
"_values",
|
87
|
-
)
|
84
|
+
__slots__ = ("device", "description", "_pending_update", "_index", "_values")
|
88
85
|
|
89
86
|
device: Device
|
90
87
|
description: ParameterDescription
|
91
88
|
_pending_update: bool
|
92
|
-
_previous_value: int
|
93
89
|
_index: int
|
94
90
|
_values: ParameterValues
|
95
91
|
|
@@ -103,8 +99,8 @@ class Parameter(ABC):
|
|
103
99
|
"""Initialize a new parameter."""
|
104
100
|
self.device = device
|
105
101
|
self.description = description
|
102
|
+
self._index = index
|
106
103
|
self._pending_update = False
|
107
|
-
self._previous_value = 0
|
108
104
|
self._index = index
|
109
105
|
self._values = values if values else ParameterValues(0, 0, 0)
|
110
106
|
|
@@ -127,9 +123,9 @@ class Parameter(ABC):
|
|
127
123
|
handler = getattr(self.values, method_to_call)
|
128
124
|
return handler(other)
|
129
125
|
|
130
|
-
if isinstance(other, (int, float, bool)) or other in (
|
126
|
+
if isinstance(other, (int, float, bool)) or other in get_args(State):
|
131
127
|
handler = getattr(self.values.value, method_to_call)
|
132
|
-
return handler(
|
128
|
+
return handler(parameter_value_to_int(other))
|
133
129
|
else:
|
134
130
|
return NotImplemented
|
135
131
|
|
@@ -184,46 +180,56 @@ class Parameter(ABC):
|
|
184
180
|
)
|
185
181
|
return type(self)(self.device, self.description, values)
|
186
182
|
|
187
|
-
def validate(self, value:
|
183
|
+
def validate(self, value: NumericType | State | bool) -> int:
|
188
184
|
"""Validate a parameter value."""
|
189
|
-
|
190
|
-
if
|
185
|
+
int_value = parameter_value_to_int(value)
|
186
|
+
if int_value < self.values.min_value or int_value > self.values.max_value:
|
191
187
|
raise ValueError(
|
192
188
|
f"Invalid value: {value}. Must be between "
|
193
189
|
f"{self.min_value} and {self.max_value}."
|
194
190
|
)
|
195
191
|
|
196
|
-
return
|
192
|
+
return int_value
|
197
193
|
|
198
194
|
async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
|
199
195
|
"""Set a parameter value."""
|
200
|
-
return await self.
|
196
|
+
return await self._attempt_update(self.validate(value), retries, timeout)
|
201
197
|
|
202
198
|
def set_nowait(self, value: Any, retries: int = 5, timeout: float = 5.0) -> None:
|
203
199
|
"""Set a parameter value without waiting."""
|
204
|
-
self.device.create_task(
|
200
|
+
self.device.create_task(
|
201
|
+
self._attempt_update(self.validate(value), retries, timeout)
|
202
|
+
)
|
205
203
|
|
206
|
-
async def
|
207
|
-
self, value:
|
204
|
+
async def _attempt_update(
|
205
|
+
self, value: int, retries: int = 5, timeout: float = 5.0
|
208
206
|
) -> bool:
|
209
|
-
"""
|
207
|
+
"""Attempt to update a parameter value on the remote device."""
|
208
|
+
_LOGGER.debug(
|
209
|
+
"Attempting to update '%s' parameter to %d", self.description.name, value
|
210
|
+
)
|
210
211
|
if value == self.values.value:
|
211
212
|
# Value is unchanged
|
212
213
|
return True
|
213
214
|
|
214
|
-
self._previous_value = self._values.value
|
215
215
|
self._values.value = value
|
216
|
+
request = await self.create_request()
|
217
|
+
if self.description.optimistic or not (initial_retries := retries):
|
218
|
+
# No retries
|
219
|
+
await self.device.queue.put(request)
|
220
|
+
return True
|
221
|
+
|
216
222
|
self._pending_update = True
|
217
223
|
while self.pending_update:
|
218
224
|
if retries <= 0:
|
219
225
|
_LOGGER.warning(
|
220
|
-
"Unable to confirm
|
226
|
+
"Unable to confirm update of '%s' parameter after %d retries",
|
221
227
|
self.description.name,
|
222
|
-
|
228
|
+
initial_retries,
|
223
229
|
)
|
224
230
|
return False
|
225
231
|
|
226
|
-
await self.device.queue.put(
|
232
|
+
await self.device.queue.put(request)
|
227
233
|
await asyncio.sleep(timeout)
|
228
234
|
retries -= 1
|
229
235
|
|
@@ -231,9 +237,7 @@ class Parameter(ABC):
|
|
231
237
|
|
232
238
|
def update(self, values: ParameterValues) -> None:
|
233
239
|
"""Update the parameter values."""
|
234
|
-
|
235
|
-
self._pending_update = False
|
236
|
-
|
240
|
+
self._pending_update = False
|
237
241
|
self._values = values
|
238
242
|
|
239
243
|
@property
|
@@ -267,17 +271,17 @@ class Parameter(ABC):
|
|
267
271
|
|
268
272
|
@property
|
269
273
|
@abstractmethod
|
270
|
-
def value(self) ->
|
274
|
+
def value(self) -> NumericType | State | bool:
|
271
275
|
"""Return the value."""
|
272
276
|
|
273
277
|
@property
|
274
278
|
@abstractmethod
|
275
|
-
def min_value(self) ->
|
279
|
+
def min_value(self) -> NumericType | State | bool:
|
276
280
|
"""Return the minimum allowed value."""
|
277
281
|
|
278
282
|
@property
|
279
283
|
@abstractmethod
|
280
|
-
def max_value(self) ->
|
284
|
+
def max_value(self) -> NumericType | State | bool:
|
281
285
|
"""Return the maximum allowed value."""
|
282
286
|
|
283
287
|
@abstractmethod
|
@@ -301,13 +305,13 @@ class Number(Parameter):
|
|
301
305
|
description: NumberDescription
|
302
306
|
|
303
307
|
async def set(
|
304
|
-
self, value:
|
308
|
+
self, value: NumericType, retries: int = 5, timeout: float = 5.0
|
305
309
|
) -> bool:
|
306
310
|
"""Set a parameter value."""
|
307
311
|
return await super().set(value, retries, timeout)
|
308
312
|
|
309
313
|
def set_nowait(
|
310
|
-
self, value:
|
314
|
+
self, value: NumericType, retries: int = 5, timeout: float = 5.0
|
311
315
|
) -> None:
|
312
316
|
"""Set a parameter value without waiting."""
|
313
317
|
super().set_nowait(value, retries, timeout)
|
@@ -317,17 +321,17 @@ class Number(Parameter):
|
|
317
321
|
return Request()
|
318
322
|
|
319
323
|
@property
|
320
|
-
def value(self) ->
|
324
|
+
def value(self) -> NumericType:
|
321
325
|
"""Return the value."""
|
322
326
|
return self.values.value
|
323
327
|
|
324
328
|
@property
|
325
|
-
def min_value(self) ->
|
329
|
+
def min_value(self) -> NumericType:
|
326
330
|
"""Return the minimum allowed value."""
|
327
331
|
return self.values.min_value
|
328
332
|
|
329
333
|
@property
|
330
|
-
def max_value(self) ->
|
334
|
+
def max_value(self) -> NumericType:
|
331
335
|
"""Return the maximum allowed value."""
|
332
336
|
return self.values.max_value
|
333
337
|
|
@@ -351,13 +355,13 @@ class Switch(Parameter):
|
|
351
355
|
description: SwitchDescription
|
352
356
|
|
353
357
|
async def set(
|
354
|
-
self, value:
|
358
|
+
self, value: State | bool, retries: int = 5, timeout: float = 5.0
|
355
359
|
) -> bool:
|
356
360
|
"""Set a parameter value."""
|
357
361
|
return await super().set(value, retries, timeout)
|
358
362
|
|
359
363
|
def set_nowait(
|
360
|
-
self, value:
|
364
|
+
self, value: State | bool, retries: int = 5, timeout: float = 5.0
|
361
365
|
) -> None:
|
362
366
|
"""Set a switch value without waiting."""
|
363
367
|
super().set_nowait(value, retries, timeout)
|
@@ -393,7 +397,7 @@ class Switch(Parameter):
|
|
393
397
|
return Request()
|
394
398
|
|
395
399
|
@property
|
396
|
-
def value(self) ->
|
400
|
+
def value(self) -> State:
|
397
401
|
"""Return the value."""
|
398
402
|
return STATE_ON if self.values.value == 1 else STATE_OFF
|
399
403
|
|
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:
|
@@ -825,7 +825,9 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
|
|
825
825
|
),
|
826
826
|
}
|
827
827
|
|
828
|
-
ECOMAX_CONTROL_PARAMETER = EcomaxSwitchDescription(
|
828
|
+
ECOMAX_CONTROL_PARAMETER = EcomaxSwitchDescription(
|
829
|
+
name=ATTR_ECOMAX_CONTROL, optimistic=True
|
830
|
+
)
|
829
831
|
THERMOSTAT_PROFILE_PARAMETER = EcomaxNumberDescription(name=ATTR_THERMOSTAT_PROFILE)
|
830
832
|
|
831
833
|
|
File without changes
|
File without changes
|
File without changes
|