PyPlumIO 0.5.28__py3-none-any.whl → 0.5.30__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.28.dist-info → PyPlumIO-0.5.30.dist-info}/METADATA +21 -21
- {PyPlumIO-0.5.28.dist-info → PyPlumIO-0.5.30.dist-info}/RECORD +21 -21
- {PyPlumIO-0.5.28.dist-info → PyPlumIO-0.5.30.dist-info}/WHEEL +1 -1
- pyplumio/_version.py +2 -2
- pyplumio/devices/__init__.py +42 -5
- pyplumio/devices/ecomax.py +2 -31
- pyplumio/devices/mixer.py +1 -1
- pyplumio/devices/thermostat.py +1 -1
- pyplumio/filters.py +10 -10
- pyplumio/frames/__init__.py +13 -6
- pyplumio/helpers/data_types.py +7 -7
- pyplumio/helpers/parameter.py +38 -2
- pyplumio/helpers/schedule.py +1 -1
- pyplumio/stream.py +33 -31
- pyplumio/structures/ecomax_parameters.py +37 -26
- pyplumio/structures/mixer_parameters.py +20 -10
- pyplumio/structures/regulator_data.py +1 -1
- pyplumio/structures/schedules.py +13 -2
- pyplumio/structures/thermostat_parameters.py +24 -6
- {PyPlumIO-0.5.28.dist-info → PyPlumIO-0.5.30.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.28.dist-info → PyPlumIO-0.5.30.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: PyPlumIO
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.30
|
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
|
@@ -22,27 +22,27 @@ Classifier: Topic :: Home Automation
|
|
22
22
|
Requires-Python: >=3.9
|
23
23
|
Description-Content-Type: text/markdown
|
24
24
|
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 ==3.8.0 ; extra == 'dev'
|
31
|
-
Requires-Dist: tomli ==2.0.1 ; extra == 'dev'
|
32
|
-
Provides-Extra: docs
|
33
|
-
Requires-Dist: sphinx ==7.4.7 ; extra == 'docs'
|
34
|
-
Requires-Dist: sphinx-rtd-theme ==2.0.0 ; extra == 'docs'
|
35
|
-
Requires-Dist: readthedocs-sphinx-search ==0.3.2 ; extra == 'docs'
|
25
|
+
Requires-Dist: dataslots==1.2.0
|
26
|
+
Requires-Dist: pyserial-asyncio==0.6
|
27
|
+
Requires-Dist: typing-extensions==4.12.2
|
36
28
|
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
|
29
|
+
Requires-Dist: codespell==2.3.0; extra == "test"
|
30
|
+
Requires-Dist: coverage==7.6.8; extra == "test"
|
31
|
+
Requires-Dist: mypy==1.13.0; extra == "test"
|
32
|
+
Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
|
33
|
+
Requires-Dist: pytest==8.3.4; extra == "test"
|
34
|
+
Requires-Dist: pytest-asyncio==0.24.0; extra == "test"
|
35
|
+
Requires-Dist: ruff==0.8.1; extra == "test"
|
36
|
+
Requires-Dist: tox==4.23.2; extra == "test"
|
37
|
+
Requires-Dist: types-pyserial==3.5.0.20240826; extra == "test"
|
38
|
+
Provides-Extra: docs
|
39
|
+
Requires-Dist: sphinx==8.1.3; extra == "docs"
|
40
|
+
Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
|
41
|
+
Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
|
42
|
+
Provides-Extra: dev
|
43
|
+
Requires-Dist: pyplumio[docs,test]; extra == "dev"
|
44
|
+
Requires-Dist: pre-commit==4.0.1; extra == "dev"
|
45
|
+
Requires-Dist: tomli==2.2.1; extra == "dev"
|
46
46
|
|
47
47
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
48
48
|
[](https://badge.fury.io/py/PyPlumIO)
|
@@ -1,29 +1,29 @@
|
|
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=PRcySwknPDLJzIxasFyVZNYI8LFj_28fjnJMHK2q0Ko,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=KK_AV_EHy5gj9s9BNZbn9i0RnT3uZsdEg6gdve1WYrY,11152
|
8
8
|
pyplumio/protocol.py,sha256=VRxrj8vZ1FMawqblKkyxg_V61TBSvVynd9u0JXYnMUU,8090
|
9
9
|
pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
pyplumio/stream.py,sha256=
|
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=YN09bGsyJ5WVmF8_-veUckqUl57eBwmfNr8M5qhyqy0,8149
|
13
|
+
pyplumio/devices/ecomax.py,sha256=ybFLJN7O3unBcyzuVmYTssBv86bPiiTGvFpFJezwUE4,15478
|
14
14
|
pyplumio/devices/ecoster.py,sha256=jNWli7ye9T6yfkcFJZhhUHH7KOv-L6AgYFp_dKyv3OM,263
|
15
|
-
pyplumio/devices/mixer.py,sha256=
|
16
|
-
pyplumio/devices/thermostat.py,sha256
|
17
|
-
pyplumio/frames/__init__.py,sha256=
|
15
|
+
pyplumio/devices/mixer.py,sha256=CnHWrJELtFgs2YTHGpQwKr2UTRdetX76OvLBA2PH-fs,3207
|
16
|
+
pyplumio/devices/thermostat.py,sha256=-CZNRyywoDU6csFu85KSmQ5woVXY0x6peXkeOsi_fqg,2617
|
17
|
+
pyplumio/frames/__init__.py,sha256=30ECFT_5IneUrpOJGxjHyeuX-i4S1ikX8Pg1HO8Yxkg,7686
|
18
18
|
pyplumio/frames/messages.py,sha256=iDwZOPdVOZaIcEHYnkwtCazH_N6BjyEDtiJBjTRaePY,3570
|
19
19
|
pyplumio/frames/requests.py,sha256=nbSuOLue2rI4WgtXslqTGfFnWBlwzLE6I9wraKC1uqg,6854
|
20
20
|
pyplumio/frames/responses.py,sha256=Ch1AVBmD6Ek7BazoEMDDEa6ad_fUdUXf4bNssQOu0sI,6228
|
21
21
|
pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
|
22
|
-
pyplumio/helpers/data_types.py,sha256=
|
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=
|
26
|
-
pyplumio/helpers/schedule.py,sha256=
|
25
|
+
pyplumio/helpers/parameter.py,sha256=yO3KSX8FtU-dMe5FzNW-RXs5yW-lYu2F5Po-4ghcSnM,12533
|
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
|
29
29
|
pyplumio/helpers/uid.py,sha256=J7gN8i8LE0g6tfL66BJbwsQQqzBBxWx7giyvqaJh4BM,976
|
@@ -31,13 +31,13 @@ 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=ki9YSzVRhCD_rYHFogJoyk0NwyNlWdZrHcp-rrCuKCU,28440
|
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
|
38
38
|
pyplumio/structures/fuel_level.py,sha256=mJpp1dnRD1wXi_6EyNX7TNXosjcr905rSHOnuZ5VD74,1069
|
39
39
|
pyplumio/structures/lambda_sensor.py,sha256=JNSCiBJoM8Uk3OGbmFIigaLOntQST5U_UrmCpaQBlM0,1595
|
40
|
-
pyplumio/structures/mixer_parameters.py,sha256=
|
40
|
+
pyplumio/structures/mixer_parameters.py,sha256=S_YOigzM9TsdEjk5EluME5fJ8owO84-xOFT9tTdBgEo,9553
|
41
41
|
pyplumio/structures/mixer_sensors.py,sha256=-cN7U-Fr2fmAQ5McQL7bZUC8CFlb1y8TN0f_dqy3UK0,2312
|
42
42
|
pyplumio/structures/modules.py,sha256=oXUIqrOAV1dZzBV5zUH3HDUSFvNOjpUSx0TF9nZVnbs,2569
|
43
43
|
pyplumio/structures/network_info.py,sha256=kPxmIaDGm5SyLRKVFzcrODlUtB0u5JjiZqekoKSyDpA,4159
|
@@ -46,15 +46,15 @@ pyplumio/structures/outputs.py,sha256=1xsJPkjN643-aFawqVoupGatUIUJfQG_g252n051Qi
|
|
46
46
|
pyplumio/structures/pending_alerts.py,sha256=Uq9WpB4MW9AhDkqmDhk-g0J0h4pVq0Q50z12dYEv6kY,739
|
47
47
|
pyplumio/structures/product_info.py,sha256=uiEN6DFQlzmBvQByTirFzXQShoex0YGdFS9WI-MAxPc,2405
|
48
48
|
pyplumio/structures/program_version.py,sha256=R-medELYHDlk_ALsw5HOVbZRb7JD3yBUsGwqwVCjrkU,2550
|
49
|
-
pyplumio/structures/regulator_data.py,sha256=
|
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=rKPWC5qcKAh6xxs91oY23NqQcWby8e3gXrD6qJAVDGo,7153
|
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=6r8_EU9T1IJ0vzZMduiLzsAaaWr6cKW_zHHAGsfGZI4,8804
|
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.30.dist-info/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
57
|
+
PyPlumIO-0.5.30.dist-info/METADATA,sha256=KMMtx2B4gTlnFBoLVzVHGs75Od9qf6BN1tH3pqeYgqM,5458
|
58
|
+
PyPlumIO-0.5.30.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
59
|
+
PyPlumIO-0.5.30.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
60
|
+
PyPlumIO-0.5.30.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:
|
@@ -45,7 +49,7 @@ class Device(ABC, EventManager):
|
|
45
49
|
|
46
50
|
queue: asyncio.Queue[Frame]
|
47
51
|
|
48
|
-
def __init__(self, queue: asyncio.Queue[Frame]):
|
52
|
+
def __init__(self, queue: asyncio.Queue[Frame]) -> None:
|
49
53
|
"""Initialize a new device."""
|
50
54
|
super().__init__()
|
51
55
|
self.queue = queue
|
@@ -125,15 +129,48 @@ class PhysicalDevice(Device, ABC):
|
|
125
129
|
address: ClassVar[int]
|
126
130
|
_network: NetworkInfo
|
127
131
|
_setup_frames: tuple[DataFrameDescription, ...]
|
132
|
+
_frame_versions: dict[int, int]
|
128
133
|
|
129
|
-
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
|
134
|
+
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
130
135
|
"""Initialize a new physical device."""
|
131
136
|
super().__init__(queue)
|
132
137
|
self._network = network
|
138
|
+
self._frame_versions = {}
|
139
|
+
|
140
|
+
async def update_frame_versions(versions: dict[int, int]) -> None:
|
141
|
+
"""Check frame versions and update outdated frames."""
|
142
|
+
for frame_type, version in versions.items():
|
143
|
+
if (
|
144
|
+
is_known_frame_type(frame_type)
|
145
|
+
and self.supports_frame_type(frame_type)
|
146
|
+
and not self.has_frame_version(frame_type, version)
|
147
|
+
):
|
148
|
+
_LOGGER.debug(
|
149
|
+
"Updating frame %s to version %i", repr(frame_type), version
|
150
|
+
)
|
151
|
+
request = await Request.create(frame_type, recipient=self.address)
|
152
|
+
self.queue.put_nowait(request)
|
153
|
+
self._frame_versions[frame_type] = version
|
154
|
+
|
155
|
+
self.subscribe(ATTR_FRAME_VERSIONS, update_frame_versions)
|
156
|
+
|
157
|
+
def has_frame_version(self, frame_type: int, version: int | None = None) -> bool:
|
158
|
+
"""Return True if frame data is up to date, False otherwise."""
|
159
|
+
if frame_type not in self._frame_versions:
|
160
|
+
return False
|
161
|
+
|
162
|
+
if version is None or self._frame_versions[frame_type] == version:
|
163
|
+
return True
|
164
|
+
|
165
|
+
return False
|
166
|
+
|
167
|
+
def supports_frame_type(self, frame_type: int) -> bool:
|
168
|
+
"""Check if frame type is supported by the device."""
|
169
|
+
return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])
|
133
170
|
|
134
171
|
def handle_frame(self, frame: Frame) -> None:
|
135
172
|
"""Handle frame received from the device."""
|
136
|
-
frame.
|
173
|
+
frame.assign_to(self)
|
137
174
|
if frame.data is not None:
|
138
175
|
for name, value in frame.data.items():
|
139
176
|
self.dispatch_nowait(name, value)
|
@@ -188,7 +225,7 @@ class VirtualDevice(Device, ABC):
|
|
188
225
|
|
189
226
|
def __init__(
|
190
227
|
self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
|
191
|
-
):
|
228
|
+
) -> None:
|
192
229
|
"""Initialize a new sub-device."""
|
193
230
|
super().__init__(queue)
|
194
231
|
self.parent = parent
|
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
|
-
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
|
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/devices/mixer.py
CHANGED
@@ -30,7 +30,7 @@ class Mixer(VirtualDevice):
|
|
30
30
|
|
31
31
|
def __init__(
|
32
32
|
self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
|
33
|
-
):
|
33
|
+
) -> None:
|
34
34
|
"""Initialize a new mixer."""
|
35
35
|
super().__init__(queue, parent, index)
|
36
36
|
self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
|
pyplumio/devices/thermostat.py
CHANGED
@@ -26,7 +26,7 @@ class Thermostat(VirtualDevice):
|
|
26
26
|
|
27
27
|
def __init__(
|
28
28
|
self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
|
29
|
-
):
|
29
|
+
) -> None:
|
30
30
|
"""Initialize a new thermostat."""
|
31
31
|
super().__init__(queue, parent, index)
|
32
32
|
self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
|
pyplumio/filters.py
CHANGED
@@ -66,13 +66,12 @@ def _significantly_changed(
|
|
66
66
|
def _significantly_changed(old: Comparable, new: Comparable) -> bool:
|
67
67
|
"""Check if value is significantly changed."""
|
68
68
|
if isinstance(old, Parameter) and isinstance(new, Parameter):
|
69
|
-
|
70
|
-
elif isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
|
71
|
-
result = not math.isclose(old, new, abs_tol=TOLERANCE)
|
72
|
-
else:
|
73
|
-
result = old != new
|
69
|
+
return new.pending_update or old.values.__ne__(new.values)
|
74
70
|
|
75
|
-
|
71
|
+
if isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
|
72
|
+
return not math.isclose(old, new, abs_tol=TOLERANCE)
|
73
|
+
|
74
|
+
return old.__ne__(new)
|
76
75
|
|
77
76
|
|
78
77
|
@overload
|
@@ -91,10 +90,11 @@ def _diffence_between(
|
|
91
90
|
"""Return a difference between values."""
|
92
91
|
if isinstance(old, list) and isinstance(new, list):
|
93
92
|
return [x for x in new if x not in old]
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
93
|
+
|
94
|
+
if isinstance(old, SupportsSubtraction) and isinstance(new, SupportsSubtraction):
|
95
|
+
return new.__sub__(old)
|
96
|
+
|
97
|
+
return None
|
98
98
|
|
99
99
|
|
100
100
|
class Filter(ABC):
|
pyplumio/frames/__init__.py
CHANGED
@@ -24,6 +24,7 @@ ECONET_VERSION: Final = 5
|
|
24
24
|
|
25
25
|
# Frame header structure.
|
26
26
|
struct_header = struct.Struct("<BH4B")
|
27
|
+
HEADER_SIZE = struct_header.size
|
27
28
|
|
28
29
|
if TYPE_CHECKING:
|
29
30
|
from pyplumio.devices import PhysicalDevice
|
@@ -73,22 +74,20 @@ class Frame(ABC):
|
|
73
74
|
|
74
75
|
__slots__ = (
|
75
76
|
"recipient",
|
76
|
-
"recipient_device",
|
77
77
|
"sender",
|
78
|
-
"sender_device",
|
79
78
|
"econet_type",
|
80
79
|
"econet_version",
|
80
|
+
"_handler",
|
81
81
|
"_message",
|
82
82
|
"_data",
|
83
83
|
)
|
84
84
|
|
85
85
|
recipient: DeviceType
|
86
|
-
recipient_device: PhysicalDevice | None
|
87
86
|
sender: DeviceType
|
88
|
-
sender_device: PhysicalDevice | None
|
89
87
|
econet_type: int
|
90
88
|
econet_version: int
|
91
89
|
frame_type: ClassVar[FrameType]
|
90
|
+
_handler: PhysicalDevice | None
|
92
91
|
_message: bytearray | None
|
93
92
|
_data: dict[str, Any] | None
|
94
93
|
|
@@ -104,11 +103,10 @@ class Frame(ABC):
|
|
104
103
|
) -> None:
|
105
104
|
"""Process a frame data and message."""
|
106
105
|
self.recipient = recipient
|
107
|
-
self.recipient_device = None
|
108
106
|
self.sender = sender
|
109
|
-
self.sender_device = None
|
110
107
|
self.econet_type = econet_type
|
111
108
|
self.econet_version = econet_version
|
109
|
+
self._handler = None
|
112
110
|
self._data = data if not kwargs else ensure_dict(data, kwargs)
|
113
111
|
self._message = message
|
114
112
|
|
@@ -153,6 +151,15 @@ class Frame(ABC):
|
|
153
151
|
"""Return a frame message represented as hex string."""
|
154
152
|
return self.bytes.hex(*args, **kwargs)
|
155
153
|
|
154
|
+
def assign_to(self, device: PhysicalDevice) -> None:
|
155
|
+
"""Assign device to the frame."""
|
156
|
+
self._handler = device
|
157
|
+
|
158
|
+
@property
|
159
|
+
def handler(self) -> PhysicalDevice | None:
|
160
|
+
"""Return the device associated to the frame."""
|
161
|
+
return self._handler
|
162
|
+
|
156
163
|
@property
|
157
164
|
def data(self) -> dict[str, Any]:
|
158
165
|
"""Return the frame data."""
|
pyplumio/helpers/data_types.py
CHANGED
@@ -19,7 +19,7 @@ class DataType(ABC, Generic[T]):
|
|
19
19
|
_value: T
|
20
20
|
_size: int
|
21
21
|
|
22
|
-
def __init__(self, value: T | None = None):
|
22
|
+
def __init__(self, value: T | None = None) -> None:
|
23
23
|
"""Initialize a new data type."""
|
24
24
|
if value is not None:
|
25
25
|
self._value = value
|
@@ -112,7 +112,7 @@ class BitArray(DataType[int]):
|
|
112
112
|
|
113
113
|
_index: int
|
114
114
|
|
115
|
-
def __init__(self, value: bool | None = None, index: int = 0):
|
115
|
+
def __init__(self, value: bool | None = None, index: int = 0) -> None:
|
116
116
|
"""Initialize a new bit array."""
|
117
117
|
super().__init__(value)
|
118
118
|
self._index = index
|
@@ -199,7 +199,7 @@ class String(DataType[str]):
|
|
199
199
|
|
200
200
|
__slots__ = ()
|
201
201
|
|
202
|
-
def __init__(self, value: str = ""):
|
202
|
+
def __init__(self, value: str = "") -> None:
|
203
203
|
"""Initialize a new null-terminated string data type."""
|
204
204
|
super().__init__(value)
|
205
205
|
self._size = len(self.value) + 1
|
@@ -219,7 +219,7 @@ class VarBytes(DataType[bytes]):
|
|
219
219
|
|
220
220
|
__slots__ = ()
|
221
221
|
|
222
|
-
def __init__(self, value: bytes = b""):
|
222
|
+
def __init__(self, value: bytes = b"") -> None:
|
223
223
|
"""Initialize a new variable-length bytes data type."""
|
224
224
|
super().__init__(value)
|
225
225
|
self._size = len(value) + 1
|
@@ -239,7 +239,7 @@ class VarString(DataType[str]):
|
|
239
239
|
|
240
240
|
__slots__ = ()
|
241
241
|
|
242
|
-
def __init__(self, value: str = ""):
|
242
|
+
def __init__(self, value: str = "") -> None:
|
243
243
|
"""Initialize a new variable length bytes data type."""
|
244
244
|
super().__init__(value)
|
245
245
|
self._size = len(value) + 1
|
@@ -326,7 +326,7 @@ class UnsignedInt(BuiltInDataType[int]):
|
|
326
326
|
_struct = struct.Struct("<I")
|
327
327
|
|
328
328
|
|
329
|
-
class Float(BuiltInDataType[
|
329
|
+
class Float(BuiltInDataType[float]):
|
330
330
|
"""Represents a float."""
|
331
331
|
|
332
332
|
__slots__ = ()
|
@@ -334,7 +334,7 @@ class Float(BuiltInDataType[int]):
|
|
334
334
|
_struct = struct.Struct("<f")
|
335
335
|
|
336
336
|
|
337
|
-
class Double(BuiltInDataType[
|
337
|
+
class Double(BuiltInDataType[float]):
|
338
338
|
"""Represents a double."""
|
339
339
|
|
340
340
|
__slots__ = ()
|
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
|
|
@@ -185,6 +194,7 @@ class Parameter(ABC):
|
|
185
194
|
f"Value must be between '{self.min_value}' and '{self.max_value}'"
|
186
195
|
)
|
187
196
|
|
197
|
+
self._previous_value = self._values.value
|
188
198
|
self._values.value = value
|
189
199
|
self._pending_update = True
|
190
200
|
while self.pending_update:
|
@@ -196,6 +206,9 @@ class Parameter(ABC):
|
|
196
206
|
return False
|
197
207
|
|
198
208
|
await self.device.queue.put(await self.create_request())
|
209
|
+
if not self.is_tracking_changes:
|
210
|
+
await self.force_refresh()
|
211
|
+
|
199
212
|
await asyncio.sleep(timeout)
|
200
213
|
retries -= 1
|
201
214
|
|
@@ -203,8 +216,19 @@ class Parameter(ABC):
|
|
203
216
|
|
204
217
|
def update(self, values: ParameterValues) -> None:
|
205
218
|
"""Update the parameter values."""
|
219
|
+
if self.pending_update and self._previous_value != values.value:
|
220
|
+
self._pending_update = False
|
221
|
+
|
206
222
|
self._values = values
|
207
|
-
|
223
|
+
|
224
|
+
async def force_refresh(self) -> None:
|
225
|
+
"""Refresh the parameter from remote."""
|
226
|
+
await self.device.queue.put(await self.create_refresh_request())
|
227
|
+
|
228
|
+
@property
|
229
|
+
def is_tracking_changes(self) -> bool:
|
230
|
+
"""Return True if remote's tracking changes, False otherwise."""
|
231
|
+
return False
|
208
232
|
|
209
233
|
@property
|
210
234
|
def pending_update(self) -> bool:
|
@@ -254,6 +278,10 @@ class Parameter(ABC):
|
|
254
278
|
async def create_request(self) -> Request:
|
255
279
|
"""Create a request to change the parameter."""
|
256
280
|
|
281
|
+
@abstractmethod
|
282
|
+
async def create_refresh_request(self) -> Request:
|
283
|
+
"""Create a request to refresh the parameter."""
|
284
|
+
|
257
285
|
|
258
286
|
@dataslots
|
259
287
|
@dataclass
|
@@ -286,6 +314,10 @@ class Number(Parameter):
|
|
286
314
|
"""Create a request to change the number."""
|
287
315
|
return Request()
|
288
316
|
|
317
|
+
async def create_refresh_request(self) -> Request:
|
318
|
+
"""Create a request to refresh the number."""
|
319
|
+
return Request()
|
320
|
+
|
289
321
|
@property
|
290
322
|
def value(self) -> int | float:
|
291
323
|
"""Return the value."""
|
@@ -362,6 +394,10 @@ class Switch(Parameter):
|
|
362
394
|
"""Create a request to change the switch."""
|
363
395
|
return Request()
|
364
396
|
|
397
|
+
async def create_refresh_request(self) -> Request:
|
398
|
+
"""Create a request to refresh the switch."""
|
399
|
+
return Request()
|
400
|
+
|
365
401
|
@property
|
366
402
|
def value(self) -> Literal["off", "on"]:
|
367
403
|
"""Return the value."""
|
pyplumio/helpers/schedule.py
CHANGED
pyplumio/stream.py
CHANGED
@@ -10,7 +10,14 @@ from typing import Final, NamedTuple
|
|
10
10
|
from pyplumio.const import DeviceType
|
11
11
|
from pyplumio.devices import is_known_device_type
|
12
12
|
from pyplumio.exceptions import ChecksumError, ReadError, UnknownDeviceError
|
13
|
-
from pyplumio.frames import
|
13
|
+
from pyplumio.frames import (
|
14
|
+
DELIMITER_SIZE,
|
15
|
+
FRAME_START,
|
16
|
+
HEADER_SIZE,
|
17
|
+
Frame,
|
18
|
+
bcc,
|
19
|
+
struct_header,
|
20
|
+
)
|
14
21
|
from pyplumio.helpers.timeout import timeout
|
15
22
|
|
16
23
|
READER_TIMEOUT: Final = 10
|
@@ -29,7 +36,7 @@ class FrameWriter:
|
|
29
36
|
|
30
37
|
_writer: StreamWriter
|
31
38
|
|
32
|
-
def __init__(self, writer: StreamWriter):
|
39
|
+
def __init__(self, writer: StreamWriter) -> None:
|
33
40
|
"""Initialize a new frame writer."""
|
34
41
|
self._writer = writer
|
35
42
|
|
@@ -46,7 +53,7 @@ class FrameWriter:
|
|
46
53
|
self._writer.close()
|
47
54
|
await self.wait_closed()
|
48
55
|
except (OSError, asyncio.TimeoutError):
|
49
|
-
_LOGGER.exception("Unexpected error while closing the writer")
|
56
|
+
_LOGGER.exception("Unexpected error, while closing the writer")
|
50
57
|
|
51
58
|
@timeout(WRITER_TIMEOUT)
|
52
59
|
async def wait_closed(self) -> None:
|
@@ -57,8 +64,6 @@ class FrameWriter:
|
|
57
64
|
class Header(NamedTuple):
|
58
65
|
"""Represents a frame header."""
|
59
66
|
|
60
|
-
bytes: bytes
|
61
|
-
frame_start: int
|
62
67
|
frame_length: int
|
63
68
|
recipient: int
|
64
69
|
sender: int
|
@@ -73,25 +78,28 @@ class FrameReader:
|
|
73
78
|
|
74
79
|
_reader: StreamReader
|
75
80
|
|
76
|
-
def __init__(self, reader: StreamReader):
|
81
|
+
def __init__(self, reader: StreamReader) -> None:
|
77
82
|
"""Initialize a new frame reader."""
|
78
83
|
self._reader = reader
|
79
84
|
|
80
|
-
async def _read_header(self) -> Header:
|
85
|
+
async def _read_header(self) -> tuple[Header, bytes]:
|
81
86
|
"""Locate and read a frame header.
|
82
87
|
|
83
88
|
Raise pyplumio.ReadError if header size is too small and
|
84
89
|
OSError if serial connection is broken.
|
85
90
|
"""
|
86
|
-
while buffer := await self._reader.read(
|
91
|
+
while buffer := await self._reader.read(DELIMITER_SIZE):
|
87
92
|
if FRAME_START not in buffer:
|
88
93
|
continue
|
89
94
|
|
90
|
-
|
91
|
-
|
92
|
-
|
95
|
+
try:
|
96
|
+
buffer += await self._reader.readexactly(HEADER_SIZE - DELIMITER_SIZE)
|
97
|
+
except IncompleteReadError as e:
|
98
|
+
raise ReadError(
|
99
|
+
f"Got incomplete header, while trying to read {e.expected} bytes"
|
100
|
+
) from e
|
93
101
|
|
94
|
-
return Header(
|
102
|
+
return Header(*struct_header.unpack_from(buffer)[DELIMITER_SIZE:]), buffer
|
95
103
|
|
96
104
|
raise OSError("Serial connection broken")
|
97
105
|
|
@@ -99,21 +107,16 @@ class FrameReader:
|
|
99
107
|
async def read(self) -> Frame | None:
|
100
108
|
"""Read the frame and return corresponding handler object.
|
101
109
|
|
102
|
-
Raise pyplumio.
|
103
|
-
|
104
|
-
frame
|
110
|
+
Raise pyplumio.UnknownDeviceError when sender device has an
|
111
|
+
unknown address, raise pyplumio.ReadError on unexpected frame
|
112
|
+
length or incomplete frame, raise pyplumio.ChecksumError on
|
113
|
+
incorrect frame checksum.
|
105
114
|
"""
|
106
|
-
(
|
107
|
-
|
108
|
-
_,
|
109
|
-
frame_length,
|
110
|
-
recipient,
|
111
|
-
sender,
|
112
|
-
econet_type,
|
113
|
-
econet_version,
|
114
|
-
) = await self._read_header()
|
115
|
+
header, buffer = await self._read_header()
|
116
|
+
frame_length, recipient, sender, econet_type, econet_version = header
|
115
117
|
|
116
118
|
if recipient not in (DeviceType.ECONET, DeviceType.ALL):
|
119
|
+
# Not an intended recipient, ignore the frame.
|
117
120
|
return None
|
118
121
|
|
119
122
|
if not is_known_device_type(sender):
|
@@ -123,25 +126,24 @@ class FrameReader:
|
|
123
126
|
raise ReadError(f"Unexpected frame length ({frame_length})")
|
124
127
|
|
125
128
|
try:
|
126
|
-
|
129
|
+
buffer += await self._reader.readexactly(frame_length - HEADER_SIZE)
|
127
130
|
except IncompleteReadError as e:
|
128
131
|
raise ReadError(
|
129
|
-
"Got
|
130
|
-
+ f"'{frame_length - struct_header.size}' bytes"
|
132
|
+
f"Got incomplete frame, while trying to read {e.expected} bytes"
|
131
133
|
) from e
|
132
134
|
|
133
|
-
if (checksum := bcc(
|
135
|
+
if (checksum := bcc(buffer[:-2])) and checksum != buffer[-2]:
|
134
136
|
raise ChecksumError(
|
135
|
-
f"Incorrect frame checksum ({checksum} != {
|
137
|
+
f"Incorrect frame checksum ({checksum} != {buffer[-2]})"
|
136
138
|
)
|
137
139
|
|
138
140
|
frame = await Frame.create(
|
139
|
-
frame_type=
|
141
|
+
frame_type=buffer[HEADER_SIZE],
|
140
142
|
recipient=DeviceType(recipient),
|
141
143
|
sender=DeviceType(sender),
|
142
144
|
econet_type=econet_type,
|
143
145
|
econet_version=econet_version,
|
144
|
-
message=
|
146
|
+
message=buffer[HEADER_SIZE + 1 : -2],
|
145
147
|
)
|
146
148
|
_LOGGER.debug("Received frame: %s", frame)
|
147
149
|
|
@@ -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,19 +55,20 @@ 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:
|
60
62
|
"""Create a request to change the parameter."""
|
61
63
|
handler = partial(Request.create, recipient=self.device.address)
|
62
64
|
if self.description.name == ATTR_ECOMAX_CONTROL:
|
63
|
-
|
65
|
+
return await handler(
|
64
66
|
frame_type=FrameType.REQUEST_ECOMAX_CONTROL,
|
65
67
|
data={ATTR_VALUE: self.values.value},
|
66
68
|
)
|
67
|
-
|
68
|
-
|
69
|
+
|
70
|
+
if self.description.name == ATTR_THERMOSTAT_PROFILE:
|
71
|
+
return await handler(
|
69
72
|
frame_type=FrameType.REQUEST_SET_THERMOSTAT_PARAMETER,
|
70
73
|
data={
|
71
74
|
ATTR_INDEX: self._index,
|
@@ -74,13 +77,22 @@ class EcomaxParameter(Parameter):
|
|
74
77
|
ATTR_SIZE: 1,
|
75
78
|
},
|
76
79
|
)
|
77
|
-
else:
|
78
|
-
request = await handler(
|
79
|
-
frame_type=FrameType.REQUEST_SET_ECOMAX_PARAMETER,
|
80
|
-
data={ATTR_INDEX: self._index, ATTR_VALUE: self.values.value},
|
81
|
-
)
|
82
80
|
|
83
|
-
return
|
81
|
+
return await handler(
|
82
|
+
frame_type=FrameType.REQUEST_SET_ECOMAX_PARAMETER,
|
83
|
+
data={ATTR_INDEX: self._index, ATTR_VALUE: self.values.value},
|
84
|
+
)
|
85
|
+
|
86
|
+
async def create_refresh_request(self) -> Request:
|
87
|
+
"""Create a request to refresh the parameter."""
|
88
|
+
return await Request.create(
|
89
|
+
FrameType.REQUEST_ECOMAX_PARAMETERS, recipient=self.device.address
|
90
|
+
)
|
91
|
+
|
92
|
+
@property
|
93
|
+
def is_tracking_changes(self) -> bool:
|
94
|
+
"""Return True if remote's tracking changes, False otherwise."""
|
95
|
+
return self.device.has_frame_version(FrameType.REQUEST_ECOMAX_PARAMETERS)
|
84
96
|
|
85
97
|
|
86
98
|
@dataslots
|
@@ -90,6 +102,7 @@ class EcomaxNumberDescription(EcomaxParameterDescription, NumberDescription):
|
|
90
102
|
|
91
103
|
multiplier: float = 1.0
|
92
104
|
offset: int = 0
|
105
|
+
precision: int = 6
|
93
106
|
|
94
107
|
|
95
108
|
class EcomaxNumber(EcomaxParameter, Number):
|
@@ -103,29 +116,27 @@ class EcomaxNumber(EcomaxParameter, Number):
|
|
103
116
|
self, value: float | int, retries: int = 5, timeout: float = 5.0
|
104
117
|
) -> bool:
|
105
118
|
"""Set a parameter value."""
|
106
|
-
value
|
119
|
+
value += self.description.offset
|
120
|
+
value = round(value / self.description.multiplier, self.description.precision)
|
107
121
|
return await super().set(value, retries, timeout)
|
108
122
|
|
109
123
|
@property
|
110
124
|
def value(self) -> float:
|
111
125
|
"""Return the value."""
|
112
|
-
|
113
|
-
|
114
|
-
) * self.description.multiplier
|
126
|
+
value = self.values.value - self.description.offset
|
127
|
+
return round(value * self.description.multiplier, self.description.precision)
|
115
128
|
|
116
129
|
@property
|
117
130
|
def min_value(self) -> float:
|
118
131
|
"""Return the minimum allowed value."""
|
119
|
-
|
120
|
-
|
121
|
-
) * self.description.multiplier
|
132
|
+
value = self.values.min_value - self.description.offset
|
133
|
+
return round(value * self.description.multiplier, self.description.precision)
|
122
134
|
|
123
135
|
@property
|
124
136
|
def max_value(self) -> float:
|
125
137
|
"""Return the maximum allowed value."""
|
126
|
-
|
127
|
-
|
128
|
-
) * self.description.multiplier
|
138
|
+
value = self.values.max_value - self.description.offset
|
139
|
+
return round(value * self.description.multiplier, self.description.precision)
|
129
140
|
|
130
141
|
|
131
142
|
@dataslots
|
@@ -292,11 +303,11 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
|
|
292
303
|
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
293
304
|
),
|
294
305
|
EcomaxNumberDescription(
|
295
|
-
name="
|
306
|
+
name="grate_fan_work",
|
296
307
|
unit_of_measurement=UnitOfMeasurement.SECONDS,
|
297
308
|
),
|
298
309
|
EcomaxNumberDescription(
|
299
|
-
name="
|
310
|
+
name="grate_fan_pause",
|
300
311
|
unit_of_measurement=UnitOfMeasurement.MINUTES,
|
301
312
|
),
|
302
313
|
EcomaxNumberDescription(
|
@@ -416,10 +427,10 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
|
|
416
427
|
unit_of_measurement=PERCENTAGE,
|
417
428
|
),
|
418
429
|
EcomaxNumberDescription(
|
419
|
-
name="
|
430
|
+
name="burning_off_fan_work",
|
420
431
|
),
|
421
432
|
EcomaxNumberDescription(
|
422
|
-
name="
|
433
|
+
name="burning_off_fan_pause",
|
423
434
|
),
|
424
435
|
EcomaxNumberDescription(
|
425
436
|
name="start_burning_off",
|
@@ -65,6 +65,17 @@ class MixerParameter(Parameter):
|
|
65
65
|
},
|
66
66
|
)
|
67
67
|
|
68
|
+
async def create_refresh_request(self) -> Request:
|
69
|
+
"""Create a request to refresh the parameter."""
|
70
|
+
return await Request.create(
|
71
|
+
FrameType.REQUEST_MIXER_PARAMETERS, recipient=self.device.parent.address
|
72
|
+
)
|
73
|
+
|
74
|
+
@property
|
75
|
+
def is_tracking_changes(self) -> bool:
|
76
|
+
"""Return True if remote's tracking changes, False otherwise."""
|
77
|
+
return self.device.parent.has_frame_version(FrameType.REQUEST_MIXER_PARAMETERS)
|
78
|
+
|
68
79
|
|
69
80
|
@dataslots
|
70
81
|
@dataclass
|
@@ -73,6 +84,7 @@ class MixerNumberDescription(MixerParameterDescription, NumberDescription):
|
|
73
84
|
|
74
85
|
multiplier: float = 1.0
|
75
86
|
offset: int = 0
|
87
|
+
precision: int = 6
|
76
88
|
|
77
89
|
|
78
90
|
class MixerNumber(MixerParameter, Number):
|
@@ -86,29 +98,27 @@ class MixerNumber(MixerParameter, Number):
|
|
86
98
|
self, value: int | float, retries: int = 5, timeout: float = 5.0
|
87
99
|
) -> bool:
|
88
100
|
"""Set a parameter value."""
|
89
|
-
value
|
101
|
+
value += self.description.offset
|
102
|
+
value = round(value / self.description.multiplier, self.description.precision)
|
90
103
|
return await super().set(value, retries, timeout)
|
91
104
|
|
92
105
|
@property
|
93
106
|
def value(self) -> float:
|
94
107
|
"""Return the parameter value."""
|
95
|
-
|
96
|
-
|
97
|
-
) * self.description.multiplier
|
108
|
+
value = self.values.value - self.description.offset
|
109
|
+
return round(value * self.description.multiplier, self.description.precision)
|
98
110
|
|
99
111
|
@property
|
100
112
|
def min_value(self) -> float:
|
101
113
|
"""Return the minimum allowed value."""
|
102
|
-
|
103
|
-
|
104
|
-
) * self.description.multiplier
|
114
|
+
value = self.values.min_value - self.description.offset
|
115
|
+
return round(value * self.description.multiplier, self.description.precision)
|
105
116
|
|
106
117
|
@property
|
107
118
|
def max_value(self) -> float:
|
108
119
|
"""Return the maximum allowed value."""
|
109
|
-
|
110
|
-
|
111
|
-
) * self.description.multiplier
|
120
|
+
value = self.values.max_value - self.description.offset
|
121
|
+
return round(value * self.description.multiplier, self.description.precision)
|
112
122
|
|
113
123
|
|
114
124
|
@dataslots
|
@@ -53,7 +53,7 @@ class RegulatorDataStructure(StructureDecoder):
|
|
53
53
|
message, offset + 2, data
|
54
54
|
)
|
55
55
|
|
56
|
-
if (device := self.frame.
|
56
|
+
if (device := self.frame.handler) is not None and (
|
57
57
|
schema := device.get_nowait(ATTR_REGDATA_SCHEMA, [])
|
58
58
|
):
|
59
59
|
self._bitarray_index = 0
|
pyplumio/structures/schedules.py
CHANGED
@@ -100,13 +100,24 @@ 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,
|
107
107
|
data=collect_schedule_data(schedule_name, self.device),
|
108
108
|
)
|
109
109
|
|
110
|
+
async def create_refresh_request(self) -> Request:
|
111
|
+
"""Create a request to refresh the parameter."""
|
112
|
+
return await Request.create(
|
113
|
+
FrameType.REQUEST_SCHEDULES, recipient=self.device.address
|
114
|
+
)
|
115
|
+
|
116
|
+
@property
|
117
|
+
def is_tracking_changes(self) -> bool:
|
118
|
+
"""Return True if remote's tracking changes, False otherwise."""
|
119
|
+
return self.device.has_frame_version(FrameType.REQUEST_SCHEDULES)
|
120
|
+
|
110
121
|
|
111
122
|
@dataslots
|
112
123
|
@dataclass
|
@@ -163,7 +174,7 @@ def _split_byte(byte: int) -> list[bool]:
|
|
163
174
|
|
164
175
|
def _join_bits(bits: Sequence[int | bool]) -> int:
|
165
176
|
"""Join eight bits into a single byte."""
|
166
|
-
return reduce(lambda
|
177
|
+
return reduce(lambda bit, byte: (bit << 1) | byte, bits)
|
167
178
|
|
168
179
|
|
169
180
|
class SchedulesStructure(Structure):
|
@@ -66,7 +66,7 @@ class ThermostatParameter(Parameter):
|
|
66
66
|
values: ParameterValues | None = None,
|
67
67
|
index: int = 0,
|
68
68
|
offset: int = 0,
|
69
|
-
):
|
69
|
+
) -> None:
|
70
70
|
"""Initialize a new thermostat parameter."""
|
71
71
|
self.offset = offset
|
72
72
|
super().__init__(device, description, values, index)
|
@@ -86,6 +86,20 @@ class ThermostatParameter(Parameter):
|
|
86
86
|
},
|
87
87
|
)
|
88
88
|
|
89
|
+
async def create_refresh_request(self) -> Request:
|
90
|
+
"""Create a request to refresh the parameter."""
|
91
|
+
return await Request.create(
|
92
|
+
FrameType.REQUEST_THERMOSTAT_PARAMETERS,
|
93
|
+
recipient=self.device.parent.address,
|
94
|
+
)
|
95
|
+
|
96
|
+
@property
|
97
|
+
def is_tracking_changes(self) -> bool:
|
98
|
+
"""Return True if remote's tracking changes, False otherwise."""
|
99
|
+
return self.device.parent.has_frame_version(
|
100
|
+
FrameType.REQUEST_THERMOSTAT_PARAMETERS
|
101
|
+
)
|
102
|
+
|
89
103
|
|
90
104
|
@dataslots
|
91
105
|
@dataclass
|
@@ -93,6 +107,7 @@ class ThermostatNumberDescription(ThermostatParameterDescription, NumberDescript
|
|
93
107
|
"""Represent a thermostat number description."""
|
94
108
|
|
95
109
|
multiplier: float = 1.0
|
110
|
+
precision: int = 6
|
96
111
|
|
97
112
|
|
98
113
|
class ThermostatNumber(ThermostatParameter, Number):
|
@@ -106,23 +121,26 @@ class ThermostatNumber(ThermostatParameter, Number):
|
|
106
121
|
self, value: int | float, retries: int = 5, timeout: float = 5.0
|
107
122
|
) -> bool:
|
108
123
|
"""Set a parameter value."""
|
109
|
-
value = value / self.description.multiplier
|
124
|
+
value = round(value / self.description.multiplier, self.description.precision)
|
110
125
|
return await super().set(value, retries, timeout)
|
111
126
|
|
112
127
|
@property
|
113
128
|
def value(self) -> float:
|
114
129
|
"""Return the value."""
|
115
|
-
|
130
|
+
value = self.values.value * self.description.multiplier
|
131
|
+
return round(value, self.description.precision)
|
116
132
|
|
117
133
|
@property
|
118
134
|
def min_value(self) -> float:
|
119
135
|
"""Return the minimum allowed value."""
|
120
|
-
|
136
|
+
value = self.values.min_value * self.description.multiplier
|
137
|
+
return round(value, self.description.precision)
|
121
138
|
|
122
139
|
@property
|
123
140
|
def max_value(self) -> float:
|
124
141
|
"""Return the maximum allowed value."""
|
125
|
-
|
142
|
+
value = self.values.max_value * self.description.multiplier
|
143
|
+
return round(value, self.description.precision)
|
126
144
|
|
127
145
|
|
128
146
|
@dataslots
|
@@ -247,7 +265,7 @@ class ThermostatParametersStructure(StructureDecoder):
|
|
247
265
|
self, message: bytearray, offset: int = 0, data: dict[str, Any] | None = None
|
248
266
|
) -> tuple[dict[str, Any], int]:
|
249
267
|
"""Decode bytes and return message data and offset."""
|
250
|
-
if (device := self.frame.
|
268
|
+
if (device := self.frame.handler) is not None and (
|
251
269
|
thermostats := device.get_nowait(ATTR_THERMOSTATS_AVAILABLE, 0)
|
252
270
|
) == 0:
|
253
271
|
return (
|
File without changes
|
File without changes
|