PyPlumIO 0.5.29__py3-none-any.whl → 0.5.31__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.29.dist-info → PyPlumIO-0.5.31.dist-info}/METADATA +23 -22
- {PyPlumIO-0.5.29.dist-info → PyPlumIO-0.5.31.dist-info}/RECORD +13 -13
- {PyPlumIO-0.5.29.dist-info → PyPlumIO-0.5.31.dist-info}/WHEEL +1 -1
- pyplumio/_version.py +2 -2
- pyplumio/devices/__init__.py +38 -4
- pyplumio/devices/ecomax.py +1 -30
- pyplumio/filters.py +52 -6
- pyplumio/helpers/parameter.py +35 -8
- pyplumio/structures/ecomax_parameters.py +7 -5
- pyplumio/structures/schedules.py +2 -2
- pyplumio/structures/thermostat_parameters.py +6 -11
- {PyPlumIO-0.5.29.dist-info → PyPlumIO-0.5.31.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.29.dist-info → PyPlumIO-0.5.31.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.31
|
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
|
@@ -17,32 +17,33 @@ Classifier: Programming Language :: Python :: 3.9
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
20
21
|
Classifier: Topic :: Software Development :: Libraries
|
21
22
|
Classifier: Topic :: Home Automation
|
22
23
|
Requires-Python: >=3.9
|
23
24
|
Description-Content-Type: text/markdown
|
24
25
|
License-File: LICENSE
|
25
|
-
Requires-Dist: dataslots
|
26
|
-
Requires-Dist: pyserial-asyncio
|
27
|
-
Requires-Dist: typing-extensions
|
28
|
-
Provides-Extra: dev
|
29
|
-
Requires-Dist: pyplumio[docs,test] ; extra == 'dev'
|
30
|
-
Requires-Dist: pre-commit ==4.0.1 ; extra == 'dev'
|
31
|
-
Requires-Dist: tomli ==2.0.2 ; extra == 'dev'
|
32
|
-
Provides-Extra: docs
|
33
|
-
Requires-Dist: sphinx ==8.1.3 ; extra == 'docs'
|
34
|
-
Requires-Dist: sphinx-rtd-theme ==3.0.1 ; extra == 'docs'
|
35
|
-
Requires-Dist: readthedocs-sphinx-search ==0.3.2 ; extra == 'docs'
|
26
|
+
Requires-Dist: dataslots==1.2.0
|
27
|
+
Requires-Dist: pyserial-asyncio==0.6
|
28
|
+
Requires-Dist: typing-extensions==4.12.2
|
36
29
|
Provides-Extra: test
|
37
|
-
Requires-Dist: codespell
|
38
|
-
Requires-Dist: coverage
|
39
|
-
Requires-Dist: mypy
|
40
|
-
Requires-Dist: pyserial-asyncio-fast
|
41
|
-
Requires-Dist: pytest
|
42
|
-
Requires-Dist: pytest-asyncio
|
43
|
-
Requires-Dist: ruff
|
44
|
-
Requires-Dist: tox
|
45
|
-
Requires-Dist: types-pyserial
|
30
|
+
Requires-Dist: codespell==2.3.0; extra == "test"
|
31
|
+
Requires-Dist: coverage==7.6.10; extra == "test"
|
32
|
+
Requires-Dist: mypy==1.14.1; extra == "test"
|
33
|
+
Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
|
34
|
+
Requires-Dist: pytest==8.3.4; extra == "test"
|
35
|
+
Requires-Dist: pytest-asyncio==0.25.2; extra == "test"
|
36
|
+
Requires-Dist: ruff==0.9.2; extra == "test"
|
37
|
+
Requires-Dist: tox==4.23.2; extra == "test"
|
38
|
+
Requires-Dist: types-pyserial==3.5.0.20241221; extra == "test"
|
39
|
+
Provides-Extra: docs
|
40
|
+
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
41
|
+
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
42
|
+
Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
|
43
|
+
Provides-Extra: dev
|
44
|
+
Requires-Dist: pyplumio[docs,test]; extra == "dev"
|
45
|
+
Requires-Dist: pre-commit==4.0.1; extra == "dev"
|
46
|
+
Requires-Dist: tomli==2.2.1; extra == "dev"
|
46
47
|
|
47
48
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
48
49
|
[](https://badge.fury.io/py/PyPlumIO)
|
@@ -1,16 +1,16 @@
|
|
1
1
|
pyplumio/__init__.py,sha256=ditJTIOFGJDg60atHzOpiggdUrZHpSynno7MtpZUGVk,3299
|
2
2
|
pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
|
3
|
-
pyplumio/_version.py,sha256=
|
3
|
+
pyplumio/_version.py,sha256=Kr7Tq9KjyxEp8tm444HGzXvFWTTLR8xyIJ5ABXwwKGA,413
|
4
4
|
pyplumio/connection.py,sha256=6mUbcjGxxEhMVIbzZgCqH-Ez-fcYoRj7ZbVSzpikpNA,5949
|
5
5
|
pyplumio/const.py,sha256=LyXa5aVy2KxnZq7H7F8s5SYsAgEC2UzZYMMRauliB2E,5502
|
6
6
|
pyplumio/exceptions.py,sha256=Wn-y5AJ5xfaBlHhTUVKB27_0Us8_OVHqh-sicnr9sYA,700
|
7
|
-
pyplumio/filters.py,sha256=
|
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=mtMpnUR3TfEmL5JUGXr6GnpPGBwzCokqIKDWp4vYiVg,4654
|
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=BEUpL2XxEk4YDfp4w0VX_LI4JQwK27eGWoJ6al_e1nE,8038
|
13
|
+
pyplumio/devices/ecomax.py,sha256=ybFLJN7O3unBcyzuVmYTssBv86bPiiTGvFpFJezwUE4,15478
|
14
14
|
pyplumio/devices/ecoster.py,sha256=jNWli7ye9T6yfkcFJZhhUHH7KOv-L6AgYFp_dKyv3OM,263
|
15
15
|
pyplumio/devices/mixer.py,sha256=CnHWrJELtFgs2YTHGpQwKr2UTRdetX76OvLBA2PH-fs,3207
|
16
16
|
pyplumio/devices/thermostat.py,sha256=-CZNRyywoDU6csFu85KSmQ5woVXY0x6peXkeOsi_fqg,2617
|
@@ -22,7 +22,7 @@ pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,
|
|
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=6ArzJDq3MiiMaRpMEP0kC6wJWsoqOqe32V1RCxg1478,1005
|
25
|
-
pyplumio/helpers/parameter.py,sha256=
|
25
|
+
pyplumio/helpers/parameter.py,sha256=hOGYkhd7-MG1euNdbHYN_2vXFlWPi-XLy_yAotYSyck,12311
|
26
26
|
pyplumio/helpers/schedule.py,sha256=PnVEkgthg6tHpHvZK9fXJz9VKNDyQ_7BFT4TTVEwNhI,5310
|
27
27
|
pyplumio/helpers/task_manager.py,sha256=HAd69yGTRL0zQsu-ywnbLu1UXiJzgHWuhYWA--vs4lQ,1181
|
28
28
|
pyplumio/helpers/timeout.py,sha256=JAhWNtIpcXyVILIwHWVy5mYofqbbRDGKLdTUKkQuajs,772
|
@@ -31,7 +31,7 @@ pyplumio/structures/__init__.py,sha256=EjK-5qJZ0F7lpP2b6epvTMg9cIBl4Kn91nqNkEcLw
|
|
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=wLwhTdKee-UtCU5NuRfOHRPvnjDOtDNUDeUgeO0VH8w,27974
|
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
|
@@ -48,13 +48,13 @@ pyplumio/structures/product_info.py,sha256=uiEN6DFQlzmBvQByTirFzXQShoex0YGdFS9WI
|
|
48
48
|
pyplumio/structures/program_version.py,sha256=R-medELYHDlk_ALsw5HOVbZRb7JD3yBUsGwqwVCjrkU,2550
|
49
49
|
pyplumio/structures/regulator_data.py,sha256=z2mSE-cxImn8YRr_yZCcDlIbXnKdETkN7GigV5vEJqA,2265
|
50
50
|
pyplumio/structures/regulator_data_schema.py,sha256=XM6M9ep3NyogbLPqp88mMTg8Sa9e5SFzV5I5pSYw5GY,1487
|
51
|
-
pyplumio/structures/schedules.py,sha256=
|
51
|
+
pyplumio/structures/schedules.py,sha256=_D8HmxMVvAAPb0cc_xSxXFRNwR9u-RWuyTy0Z5KscUk,6717
|
52
52
|
pyplumio/structures/statuses.py,sha256=wkoynyMRr1VREwfBC6vU48kPA8ZQ83pcXuciy2xHJrk,1166
|
53
53
|
pyplumio/structures/temperatures.py,sha256=1CDzehNmbALz1Jyt_9gZNIk52q6Wv-xQXjijVDCVYec,2337
|
54
|
-
pyplumio/structures/thermostat_parameters.py,sha256=
|
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.31.dist-info/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
57
|
+
PyPlumIO-0.5.31.dist-info/METADATA,sha256=thPWL1GWRLLFkIdMOBz4CUN4LZmGxEA_BvvWYKjR-xA,5510
|
58
|
+
PyPlumIO-0.5.31.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
59
|
+
PyPlumIO-0.5.31.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
60
|
+
PyPlumIO-0.5.31.dist-info/RECORD,,
|
pyplumio/_version.py
CHANGED
pyplumio/devices/__init__.py
CHANGED
@@ -5,17 +5,21 @@ from __future__ import annotations
|
|
5
5
|
from abc import ABC
|
6
6
|
import asyncio
|
7
7
|
from functools import cache
|
8
|
+
import logging
|
8
9
|
from typing import Any, ClassVar
|
9
10
|
|
10
11
|
from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType
|
11
12
|
from pyplumio.exceptions import UnknownDeviceError
|
12
|
-
from pyplumio.frames import DataFrameDescription, Frame, Request
|
13
|
+
from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
|
13
14
|
from pyplumio.helpers.event_manager import EventManager
|
14
15
|
from pyplumio.helpers.factory import create_instance
|
15
16
|
from pyplumio.helpers.parameter import Parameter, ParameterValue
|
17
|
+
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
|
16
18
|
from pyplumio.structures.network_info import NetworkInfo
|
17
19
|
from pyplumio.utils import to_camelcase
|
18
20
|
|
21
|
+
_LOGGER = logging.getLogger(__name__)
|
22
|
+
|
19
23
|
|
20
24
|
@cache
|
21
25
|
def is_known_device_type(device_type: int) -> bool:
|
@@ -103,9 +107,6 @@ class Device(ABC, EventManager):
|
|
103
107
|
this value is used to determine failure when
|
104
108
|
retrying and doesn't block, defaults to `None`
|
105
109
|
:type timeout: float, optional
|
106
|
-
:return: `True` if parameter was successfully set, `False`
|
107
|
-
otherwise.
|
108
|
-
:rtype: bool
|
109
110
|
"""
|
110
111
|
self.create_task(self.set(name, value, retries, timeout))
|
111
112
|
|
@@ -125,11 +126,44 @@ class PhysicalDevice(Device, ABC):
|
|
125
126
|
address: ClassVar[int]
|
126
127
|
_network: NetworkInfo
|
127
128
|
_setup_frames: tuple[DataFrameDescription, ...]
|
129
|
+
_frame_versions: dict[int, int]
|
128
130
|
|
129
131
|
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
130
132
|
"""Initialize a new physical device."""
|
131
133
|
super().__init__(queue)
|
132
134
|
self._network = network
|
135
|
+
self._frame_versions = {}
|
136
|
+
|
137
|
+
async def update_frame_versions(versions: dict[int, int]) -> None:
|
138
|
+
"""Check frame versions and update outdated frames."""
|
139
|
+
for frame_type, version in versions.items():
|
140
|
+
if (
|
141
|
+
is_known_frame_type(frame_type)
|
142
|
+
and self.supports_frame_type(frame_type)
|
143
|
+
and not self.has_frame_version(frame_type, version)
|
144
|
+
):
|
145
|
+
_LOGGER.debug(
|
146
|
+
"Updating frame %s to version %i", repr(frame_type), version
|
147
|
+
)
|
148
|
+
request = await Request.create(frame_type, recipient=self.address)
|
149
|
+
self.queue.put_nowait(request)
|
150
|
+
self._frame_versions[frame_type] = version
|
151
|
+
|
152
|
+
self.subscribe(ATTR_FRAME_VERSIONS, update_frame_versions)
|
153
|
+
|
154
|
+
def has_frame_version(self, frame_type: int, version: int | None = None) -> bool:
|
155
|
+
"""Return True if frame data is up to date, False otherwise."""
|
156
|
+
if frame_type not in self._frame_versions:
|
157
|
+
return False
|
158
|
+
|
159
|
+
if version is None or self._frame_versions[frame_type] == version:
|
160
|
+
return True
|
161
|
+
|
162
|
+
return False
|
163
|
+
|
164
|
+
def supports_frame_type(self, frame_type: int) -> bool:
|
165
|
+
"""Check if frame type is supported by the device."""
|
166
|
+
return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])
|
133
167
|
|
134
168
|
def handle_frame(self, frame: Frame) -> None:
|
135
169
|
"""Handle frame received from the device."""
|
pyplumio/devices/ecomax.py
CHANGED
@@ -9,7 +9,6 @@ import time
|
|
9
9
|
from typing import Any, Final
|
10
10
|
|
11
11
|
from pyplumio.const import (
|
12
|
-
ATTR_FRAME_ERRORS,
|
13
12
|
ATTR_PASSWORD,
|
14
13
|
ATTR_SENSORS,
|
15
14
|
ATTR_STATE,
|
@@ -21,7 +20,7 @@ from pyplumio.devices import PhysicalDevice
|
|
21
20
|
from pyplumio.devices.mixer import Mixer
|
22
21
|
from pyplumio.devices.thermostat import Thermostat
|
23
22
|
from pyplumio.filters import on_change
|
24
|
-
from pyplumio.frames import DataFrameDescription, Frame, Request
|
23
|
+
from pyplumio.frames import DataFrameDescription, Frame, Request
|
25
24
|
from pyplumio.helpers.parameter import ParameterValues
|
26
25
|
from pyplumio.helpers.schedule import Schedule, ScheduleDay
|
27
26
|
from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
|
@@ -35,7 +34,6 @@ from pyplumio.structures.ecomax_parameters import (
|
|
35
34
|
EcomaxSwitch,
|
36
35
|
EcomaxSwitchDescription,
|
37
36
|
)
|
38
|
-
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
|
39
37
|
from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
|
40
38
|
from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
|
41
39
|
from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
|
@@ -106,17 +104,14 @@ class EcoMAX(PhysicalDevice):
|
|
106
104
|
|
107
105
|
address = DeviceType.ECOMAX
|
108
106
|
|
109
|
-
_frame_versions: dict[int, int]
|
110
107
|
_fuel_burned_timestamp_ns: int
|
111
108
|
_setup_frames = SETUP_FRAME_TYPES
|
112
109
|
|
113
110
|
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
114
111
|
"""Initialize a new ecoMAX controller."""
|
115
112
|
super().__init__(queue, network)
|
116
|
-
self._frame_versions = {}
|
117
113
|
self._fuel_burned_timestamp_ns = time.perf_counter_ns()
|
118
114
|
self.subscribe(ATTR_ECOMAX_PARAMETERS, self._handle_ecomax_parameters)
|
119
|
-
self.subscribe(ATTR_FRAME_VERSIONS, self._update_frame_versions)
|
120
115
|
self.subscribe(ATTR_FUEL_CONSUMPTION, self._add_burned_fuel_counter)
|
121
116
|
self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
|
122
117
|
self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
|
@@ -142,17 +137,6 @@ class EcoMAX(PhysicalDevice):
|
|
142
137
|
|
143
138
|
super().handle_frame(frame)
|
144
139
|
|
145
|
-
def _has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
|
146
|
-
"""Check if ecoMAX controller has this version of the frame."""
|
147
|
-
return (
|
148
|
-
frame_type in self._frame_versions
|
149
|
-
and self._frame_versions[frame_type] == version
|
150
|
-
)
|
151
|
-
|
152
|
-
def _frame_is_supported(self, frame_type: FrameType | int) -> bool:
|
153
|
-
"""Check if frame is supported by the device."""
|
154
|
-
return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])
|
155
|
-
|
156
140
|
def _mixers(self, indexes: Iterable[int]) -> Generator[Mixer, None, None]:
|
157
141
|
"""Iterate through the mixer indexes.
|
158
142
|
|
@@ -224,19 +208,6 @@ class EcoMAX(PhysicalDevice):
|
|
224
208
|
await asyncio.gather(*_ecomax_parameter_events())
|
225
209
|
return True
|
226
210
|
|
227
|
-
async def _update_frame_versions(self, versions: dict[int, int]) -> None:
|
228
|
-
"""Check frame versions and update outdated frames."""
|
229
|
-
for frame_type, version in versions.items():
|
230
|
-
if (
|
231
|
-
is_known_frame_type(frame_type)
|
232
|
-
and self._frame_is_supported(frame_type)
|
233
|
-
and not self._has_frame_version(frame_type, version)
|
234
|
-
):
|
235
|
-
# We don't have this frame or it's version has changed.
|
236
|
-
request = await Request.create(frame_type, recipient=self.address)
|
237
|
-
self.queue.put_nowait(request)
|
238
|
-
self._frame_versions[frame_type] = version
|
239
|
-
|
240
211
|
async def _add_burned_fuel_counter(self, fuel_consumption: float) -> None:
|
241
212
|
"""Calculate fuel burned since last sensor's data message."""
|
242
213
|
current_timestamp_ns = time.perf_counter_ns()
|
pyplumio/filters.py
CHANGED
@@ -125,6 +125,52 @@ class Filter(ABC):
|
|
125
125
|
"""Set a new value for the callback."""
|
126
126
|
|
127
127
|
|
128
|
+
class _Clamp(Filter):
|
129
|
+
"""Represents a clamp filter.
|
130
|
+
|
131
|
+
Calls callback with a value clamped between specified boundaries.
|
132
|
+
"""
|
133
|
+
|
134
|
+
__slots__ = ("_min_value", "_max_value")
|
135
|
+
|
136
|
+
_min_value: float
|
137
|
+
_max_value: float
|
138
|
+
|
139
|
+
def __init__(self, callback: Callback, min_value: float, max_value: float) -> None:
|
140
|
+
"""Initialize a new clamp filter."""
|
141
|
+
super().__init__(callback)
|
142
|
+
self._min_value = min_value
|
143
|
+
self._max_value = max_value
|
144
|
+
|
145
|
+
async def __call__(self, new_value: Any) -> Any:
|
146
|
+
"""Set a new value for the callback."""
|
147
|
+
if new_value < self._min_value:
|
148
|
+
return await self._callback(self._min_value)
|
149
|
+
|
150
|
+
if new_value > self._max_value:
|
151
|
+
return await self._callback(self._max_value)
|
152
|
+
|
153
|
+
return await self._callback(new_value)
|
154
|
+
|
155
|
+
|
156
|
+
def clamp(callback: Callback, min_value: float, max_value: float) -> _Clamp:
|
157
|
+
"""Return a clamp filter.
|
158
|
+
|
159
|
+
A callback function will be called with value clamped between
|
160
|
+
specified boundaries.
|
161
|
+
|
162
|
+
:param callback: A callback function to be awaited on new value
|
163
|
+
:type callback: Callback
|
164
|
+
:param min_value: A lower boundary
|
165
|
+
:type min_value: float
|
166
|
+
:param max_value: An upper boundary
|
167
|
+
:type max_value: float
|
168
|
+
:return: An instance of callable filter
|
169
|
+
:rtype: _Clamp
|
170
|
+
"""
|
171
|
+
return _Clamp(callback, min_value, max_value)
|
172
|
+
|
173
|
+
|
128
174
|
class _OnChange(Filter):
|
129
175
|
"""Represents a value changed filter.
|
130
176
|
|
@@ -151,7 +197,7 @@ def on_change(callback: Callback) -> _OnChange:
|
|
151
197
|
|
152
198
|
:param callback: A callback function to be awaited on value change
|
153
199
|
:type callback: Callback
|
154
|
-
:return:
|
200
|
+
:return: An instance of callable filter
|
155
201
|
:rtype: _OnChange
|
156
202
|
"""
|
157
203
|
return _OnChange(callback)
|
@@ -201,7 +247,7 @@ def debounce(callback: Callback, min_calls: int) -> _Debounce:
|
|
201
247
|
:param min_calls: Value shouldn't change for this amount of
|
202
248
|
filter calls
|
203
249
|
:type min_calls: int
|
204
|
-
:return:
|
250
|
+
:return: An instance of callable filter
|
205
251
|
:rtype: _Debounce
|
206
252
|
"""
|
207
253
|
return _Debounce(callback, min_calls)
|
@@ -248,7 +294,7 @@ def throttle(callback: Callback, seconds: float) -> _Throttle:
|
|
248
294
|
:param seconds: A callback will be awaited at most once per
|
249
295
|
this amount of seconds
|
250
296
|
:type seconds: float
|
251
|
-
:return:
|
297
|
+
:return: An instance of callable filter
|
252
298
|
:rtype: _Throttle
|
253
299
|
"""
|
254
300
|
return _Throttle(callback, seconds)
|
@@ -285,7 +331,7 @@ def delta(callback: Callback) -> _Delta:
|
|
285
331
|
:param callback: A callback function that will be awaited with
|
286
332
|
difference between values in two subsequent calls
|
287
333
|
:type callback: Callback
|
288
|
-
:return:
|
334
|
+
:return: An instance of callable filter
|
289
335
|
:rtype: _Delta
|
290
336
|
"""
|
291
337
|
return _Delta(callback)
|
@@ -340,7 +386,7 @@ def aggregate(callback: Callback, seconds: float) -> _Aggregate:
|
|
340
386
|
:param seconds: A callback will be awaited with a sum of values
|
341
387
|
aggregated over this amount of seconds.
|
342
388
|
:type seconds: float
|
343
|
-
:return:
|
389
|
+
:return: An instance of callable filter
|
344
390
|
:rtype: _Aggregate
|
345
391
|
"""
|
346
392
|
return _Aggregate(callback, seconds)
|
@@ -382,7 +428,7 @@ def custom(callback: Callback, filter_fn: Callable[[Any], bool]) -> _Custom:
|
|
382
428
|
:param filter_fn: Filter function, that will be called with a
|
383
429
|
value and should return `True` to await filter's callback
|
384
430
|
:type filter_fn: Callable[[Any], bool]
|
385
|
-
:return:
|
431
|
+
:return: An instance of callable filter
|
386
432
|
:rtype: _Custom
|
387
433
|
"""
|
388
434
|
return _Custom(callback, filter_fn)
|
pyplumio/helpers/parameter.py
CHANGED
@@ -77,11 +77,19 @@ class ParameterDescription:
|
|
77
77
|
class Parameter(ABC):
|
78
78
|
"""Represents a base parameter."""
|
79
79
|
|
80
|
-
__slots__ = (
|
80
|
+
__slots__ = (
|
81
|
+
"device",
|
82
|
+
"description",
|
83
|
+
"_pending_update",
|
84
|
+
"_previous_value",
|
85
|
+
"_index",
|
86
|
+
"_values",
|
87
|
+
)
|
81
88
|
|
82
89
|
device: Device
|
83
90
|
description: ParameterDescription
|
84
91
|
_pending_update: bool
|
92
|
+
_previous_value: int
|
85
93
|
_index: int
|
86
94
|
_values: ParameterValues
|
87
95
|
|
@@ -96,6 +104,7 @@ class Parameter(ABC):
|
|
96
104
|
self.device = device
|
97
105
|
self.description = description
|
98
106
|
self._pending_update = False
|
107
|
+
self._previous_value = 0
|
99
108
|
self._index = index
|
100
109
|
self._values = values if values else ParameterValues(0, 0, 0)
|
101
110
|
|
@@ -175,16 +184,32 @@ class Parameter(ABC):
|
|
175
184
|
)
|
176
185
|
return type(self)(self.device, self.description, values)
|
177
186
|
|
178
|
-
|
179
|
-
"""
|
180
|
-
|
181
|
-
|
187
|
+
def validate(self, value: ParameterValue) -> int:
|
188
|
+
"""Validate a parameter value."""
|
189
|
+
value = _normalize_parameter_value(value)
|
190
|
+
if value == self.values.value:
|
191
|
+
raise ValueError("Parameter value is unchanged.")
|
182
192
|
|
183
193
|
if value < self.values.min_value or value > self.values.max_value:
|
184
194
|
raise ValueError(
|
185
195
|
f"Value must be between '{self.min_value}' and '{self.max_value}'"
|
186
196
|
)
|
187
197
|
|
198
|
+
return value
|
199
|
+
|
200
|
+
async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
|
201
|
+
"""Set a parameter value."""
|
202
|
+
return await self._try_set(self.validate(value), retries, timeout)
|
203
|
+
|
204
|
+
def set_nowait(self, value: Any, retries: int = 5, timeout: float = 5.0) -> None:
|
205
|
+
"""Set a parameter value without waiting."""
|
206
|
+
self.device.create_task(self._try_set(self.validate(value), retries, timeout))
|
207
|
+
|
208
|
+
async def _try_set(
|
209
|
+
self, value: Any, retries: int = 5, timeout: float = 5.0
|
210
|
+
) -> bool:
|
211
|
+
"""Try to set a parameter value."""
|
212
|
+
self._previous_value = self._values.value
|
188
213
|
self._values.value = value
|
189
214
|
self._pending_update = True
|
190
215
|
while self.pending_update:
|
@@ -203,8 +228,10 @@ class Parameter(ABC):
|
|
203
228
|
|
204
229
|
def update(self, values: ParameterValues) -> None:
|
205
230
|
"""Update the parameter values."""
|
231
|
+
if self.pending_update and self._previous_value != values.value:
|
232
|
+
self._pending_update = False
|
233
|
+
|
206
234
|
self._values = values
|
207
|
-
self._pending_update = False
|
208
235
|
|
209
236
|
@property
|
210
237
|
def pending_update(self) -> bool:
|
@@ -280,7 +307,7 @@ class Number(Parameter):
|
|
280
307
|
self, value: int | float, retries: int = 5, timeout: float = 5.0
|
281
308
|
) -> None:
|
282
309
|
"""Set a parameter value without waiting."""
|
283
|
-
|
310
|
+
super().set_nowait(value, retries, timeout)
|
284
311
|
|
285
312
|
async def create_request(self) -> Request:
|
286
313
|
"""Create a request to change the number."""
|
@@ -330,7 +357,7 @@ class Switch(Parameter):
|
|
330
357
|
self, value: bool | Literal["off", "on"], retries: int = 5, timeout: float = 5.0
|
331
358
|
) -> None:
|
332
359
|
"""Set a switch value without waiting."""
|
333
|
-
|
360
|
+
super().set_nowait(value, retries, timeout)
|
334
361
|
|
335
362
|
async def turn_on(self) -> bool:
|
336
363
|
"""Set a switch value to 'on'.
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
from collections.abc import Generator
|
6
6
|
from dataclasses import dataclass
|
7
7
|
from functools import partial
|
8
|
-
from typing import Any, Final
|
8
|
+
from typing import TYPE_CHECKING, Any, Final
|
9
9
|
|
10
10
|
from dataslots import dataslots
|
11
11
|
|
@@ -19,7 +19,6 @@ from pyplumio.const import (
|
|
19
19
|
ProductType,
|
20
20
|
UnitOfMeasurement,
|
21
21
|
)
|
22
|
-
from pyplumio.devices import PhysicalDevice
|
23
22
|
from pyplumio.frames import Request
|
24
23
|
from pyplumio.helpers.parameter import (
|
25
24
|
Number,
|
@@ -35,6 +34,9 @@ from pyplumio.structures import StructureDecoder
|
|
35
34
|
from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PROFILE
|
36
35
|
from pyplumio.utils import ensure_dict
|
37
36
|
|
37
|
+
if TYPE_CHECKING:
|
38
|
+
from pyplumio.devices.ecomax import EcoMAX
|
39
|
+
|
38
40
|
ATTR_ECOMAX_CONTROL: Final = "ecomax_control"
|
39
41
|
ATTR_ECOMAX_PARAMETERS: Final = "ecomax_parameters"
|
40
42
|
|
@@ -53,7 +55,7 @@ class EcomaxParameter(Parameter):
|
|
53
55
|
|
54
56
|
__slots__ = ()
|
55
57
|
|
56
|
-
device:
|
58
|
+
device: EcoMAX
|
57
59
|
description: EcomaxParameterDescription
|
58
60
|
|
59
61
|
async def create_request(self) -> Request:
|
@@ -466,8 +468,8 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
|
|
466
468
|
),
|
467
469
|
EcomaxNumberDescription(
|
468
470
|
name="max_fuel_flow",
|
469
|
-
multiplier=
|
470
|
-
unit_of_measurement=UnitOfMeasurement.
|
471
|
+
multiplier=20,
|
472
|
+
unit_of_measurement=UnitOfMeasurement.GRAMS,
|
471
473
|
),
|
472
474
|
EcomaxNumberDescription(
|
473
475
|
name="feeder_calibration",
|
pyplumio/structures/schedules.py
CHANGED
@@ -100,7 +100,7 @@ class ScheduleParameter(Parameter):
|
|
100
100
|
|
101
101
|
async def create_request(self) -> Request:
|
102
102
|
"""Create a request to change the parameter."""
|
103
|
-
schedule_name
|
103
|
+
schedule_name = self.description.name.split("_schedule_", 1)[0]
|
104
104
|
return await Request.create(
|
105
105
|
FrameType.REQUEST_SET_SCHEDULE,
|
106
106
|
recipient=self.device.address,
|
@@ -163,7 +163,7 @@ def _split_byte(byte: int) -> list[bool]:
|
|
163
163
|
|
164
164
|
def _join_bits(bits: Sequence[int | bool]) -> int:
|
165
165
|
"""Join eight bits into a single byte."""
|
166
|
-
return reduce(lambda
|
166
|
+
return reduce(lambda bit, byte: (bit << 1) | byte, bits)
|
167
167
|
|
168
168
|
|
169
169
|
class SchedulesStructure(Structure):
|
@@ -113,25 +113,20 @@ class ThermostatNumber(ThermostatParameter, Number):
|
|
113
113
|
@property
|
114
114
|
def value(self) -> float:
|
115
115
|
"""Return the value."""
|
116
|
-
|
117
|
-
|
118
|
-
)
|
116
|
+
value = self.values.value * self.description.multiplier
|
117
|
+
return round(value, self.description.precision)
|
119
118
|
|
120
119
|
@property
|
121
120
|
def min_value(self) -> float:
|
122
121
|
"""Return the minimum allowed value."""
|
123
|
-
|
124
|
-
|
125
|
-
self.description.precision,
|
126
|
-
)
|
122
|
+
value = self.values.min_value * self.description.multiplier
|
123
|
+
return round(value, self.description.precision)
|
127
124
|
|
128
125
|
@property
|
129
126
|
def max_value(self) -> float:
|
130
127
|
"""Return the maximum allowed value."""
|
131
|
-
|
132
|
-
|
133
|
-
self.description.precision,
|
134
|
-
)
|
128
|
+
value = self.values.max_value * self.description.multiplier
|
129
|
+
return round(value, self.description.precision)
|
135
130
|
|
136
131
|
|
137
132
|
@dataslots
|
File without changes
|
File without changes
|