PyPlumIO 0.5.27__py3-none-any.whl → 0.5.29__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.27.dist-info → PyPlumIO-0.5.29.dist-info}/METADATA +10 -10
- {PyPlumIO-0.5.27.dist-info → PyPlumIO-0.5.29.dist-info}/RECORD +20 -20
- {PyPlumIO-0.5.27.dist-info → PyPlumIO-0.5.29.dist-info}/WHEEL +1 -1
- pyplumio/_version.py +2 -2
- pyplumio/const.py +47 -0
- pyplumio/devices/__init__.py +4 -4
- pyplumio/devices/ecomax.py +1 -1
- 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/schedule.py +1 -1
- pyplumio/stream.py +33 -31
- pyplumio/structures/ecomax_parameters.py +21 -23
- pyplumio/structures/mixer_parameters.py +9 -10
- pyplumio/structures/regulator_data.py +1 -1
- pyplumio/structures/thermostat_parameters.py +15 -6
- {PyPlumIO-0.5.27.dist-info → PyPlumIO-0.5.29.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.27.dist-info → PyPlumIO-0.5.29.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.29
|
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,21 +27,21 @@ Requires-Dist: pyserial-asyncio ==0.6
|
|
27
27
|
Requires-Dist: typing-extensions ==4.12.2
|
28
28
|
Provides-Extra: dev
|
29
29
|
Requires-Dist: pyplumio[docs,test] ; extra == 'dev'
|
30
|
-
Requires-Dist: pre-commit ==
|
31
|
-
Requires-Dist: tomli ==2.0.
|
30
|
+
Requires-Dist: pre-commit ==4.0.1 ; extra == 'dev'
|
31
|
+
Requires-Dist: tomli ==2.0.2 ; extra == 'dev'
|
32
32
|
Provides-Extra: docs
|
33
|
-
Requires-Dist: sphinx ==
|
34
|
-
Requires-Dist: sphinx-rtd-theme ==
|
33
|
+
Requires-Dist: sphinx ==8.1.3 ; extra == 'docs'
|
34
|
+
Requires-Dist: sphinx-rtd-theme ==3.0.1 ; extra == 'docs'
|
35
35
|
Requires-Dist: readthedocs-sphinx-search ==0.3.2 ; extra == 'docs'
|
36
36
|
Provides-Extra: test
|
37
37
|
Requires-Dist: codespell ==2.3.0 ; extra == 'test'
|
38
|
-
Requires-Dist: coverage ==7.6.
|
39
|
-
Requires-Dist: mypy ==1.
|
38
|
+
Requires-Dist: coverage ==7.6.4 ; extra == 'test'
|
39
|
+
Requires-Dist: mypy ==1.13.0 ; extra == 'test'
|
40
40
|
Requires-Dist: pyserial-asyncio-fast ==0.14 ; extra == 'test'
|
41
|
-
Requires-Dist: pytest ==8.3.
|
41
|
+
Requires-Dist: pytest ==8.3.3 ; extra == 'test'
|
42
42
|
Requires-Dist: pytest-asyncio ==0.24.0 ; extra == 'test'
|
43
|
-
Requires-Dist: ruff ==0.
|
44
|
-
Requires-Dist: tox ==4.
|
43
|
+
Requires-Dist: ruff ==0.7.1 ; extra == 'test'
|
44
|
+
Requires-Dist: tox ==4.23.2 ; extra == 'test'
|
45
45
|
Requires-Dist: types-pyserial ==3.5.0.20240826 ; extra == 'test'
|
46
46
|
|
47
47
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
@@ -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=YLYDNYQKnQz5-FefYBZUB86Rwzn5G_lMcWd9gf_aubQ,413
|
4
4
|
pyplumio/connection.py,sha256=6mUbcjGxxEhMVIbzZgCqH-Ez-fcYoRj7ZbVSzpikpNA,5949
|
5
|
-
pyplumio/const.py,sha256=
|
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=0tDa30WPvI53uSLUu2PStIgvbst-cUvYLe3SSCRHDZc,6551
|
13
|
+
pyplumio/devices/ecomax.py,sha256=CEdU7nMyJGGVNMANEwN4fgAzf6O8ufuIN0_CQcGqO3k,16886
|
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
25
|
pyplumio/helpers/parameter.py,sha256=iCtKkYXJI0Zj4ifU3HCwoq_qqSE03giblSP1rqsWzcY,11463
|
26
|
-
pyplumio/helpers/schedule.py,sha256=
|
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=OYQZ0XJF-lqV_GdMaLTek4Gd6etIwhEJIyZyBS189O4,27959
|
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=idF3tYukfAz1EM1CE-hZBjjmGrNZN6X1MlcZr3FHrzA,9089
|
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
51
|
pyplumio/structures/schedules.py,sha256=YzlfgprZq4pDfl-NBHl-EblhxatmDYr0UOkkHBW0Jok,6707
|
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=SewPHuw7f4PTvzKXcOQA8SNjQyuyBWNIk6jsUJX82BI,8321
|
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.29.dist-info/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
57
|
+
PyPlumIO-0.5.29.dist-info/METADATA,sha256=lRGUv-VdjhGInA3PozigXBjMGH8mJg_TquWmaUSvdFA,5490
|
58
|
+
PyPlumIO-0.5.29.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
59
|
+
PyPlumIO-0.5.29.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
60
|
+
PyPlumIO-0.5.29.dist-info/RECORD,,
|
pyplumio/_version.py
CHANGED
pyplumio/const.py
CHANGED
@@ -106,7 +106,54 @@ class AlertType(IntEnum):
|
|
106
106
|
MAX_EXHAUST_TEMP_EXCEEDED = 6
|
107
107
|
KINDLING_FAILURE = 7
|
108
108
|
NO_FUEL = 8
|
109
|
+
LEAK_DETECTED = 9
|
110
|
+
PRESSURE_SENSOR_FAILURE = 10
|
109
111
|
FAN_FAILURE = 11
|
112
|
+
INSUFFICIENT_AIR_PRESSURE = 12
|
113
|
+
BURN_OFF_FAILURE = 13
|
114
|
+
FLAME_SENSOR_FAILURE = 14
|
115
|
+
LINEAR_ACTUATOR_BLOCKED = 15
|
116
|
+
INCORRECT_PARAMETERS = 16
|
117
|
+
CONDENSATION_WARNING = 17
|
118
|
+
BOILER_STB_TRIPPED = 18
|
119
|
+
FEEDER_STB_TRIPPED = 19
|
120
|
+
MIN_WATER_PRESSURE_EXCEEDED = 20
|
121
|
+
MAX_WATER_PRESSURE_EXCEEDED = 21
|
122
|
+
FEEDER_JAMMED = 22
|
123
|
+
FLAMEOUT = 23
|
124
|
+
EXHAUST_FAN_FAILURE = 24
|
125
|
+
EXTERNAL_FEEDER_FAILURE = 25
|
126
|
+
SOLAR_COLLECTOR_TEMP_SENSOR_FAILURE = 26
|
127
|
+
SOLAR_CIRCUIT_TEMP_SENSOR_FAILURE = 27
|
128
|
+
H1_CIRCUIT_TEMP_SENSOR_FAILURE = 28
|
129
|
+
H2_CIRCUIT_TEMP_SENSOR_FAILURE = 29
|
130
|
+
H3_CIRCUIT_TEMP_SENSOR_FAILURE = 30
|
131
|
+
OUTDOOR_TEMP_SENSOR_FAILURE = 31
|
132
|
+
WATER_HEATER_TEMP_SENSOR_FAILURE = 32
|
133
|
+
H0_CIRCUIT_TEMP_SENSOR_FAILURE = 33
|
134
|
+
FROST_PROTECTION_RUNNING_WO_HS = 34
|
135
|
+
FROST_PROTECTION_RUNNING_W_HS = 35
|
136
|
+
MAX_SOLAR_COLLECTOR_TEMP_EXCEEDED = 36
|
137
|
+
MAX_HEATED_FLOOR_TEMP_EXCEEDED = 37
|
138
|
+
BOILER_COOLING_RUNNING = 38
|
139
|
+
ECOLAMBDA_CONNECTION_FAILURE = 39
|
140
|
+
PRIMARY_AIR_THROTTLE_JAMMED = 40
|
141
|
+
SECONDARY_AIR_THROTTLE_JAMMED = 41
|
142
|
+
FEEDER_OVERFLOW = 42
|
143
|
+
FURNANCE_OVERFLOW = 43
|
144
|
+
MODULE_B_CONNECTION_FAILURE = 44
|
145
|
+
CLEANING_ACTUATOR_FAILURE = 45
|
146
|
+
MIN_PRESSURE_EXCEEDED = 46
|
147
|
+
MAX_PRESSURE_EXCEEDED = 47
|
148
|
+
PRESSURE_SENSOR_DAMAGED = 48
|
149
|
+
MAX_MAIN_HS_TEMP_EXCEEDED = 49
|
150
|
+
MAX_ADDITIONAL_HS_TEMP_EXCEEDED = 50
|
151
|
+
SOLAR_PANEL_OFFLINE = 51
|
152
|
+
FEEDER_CONTROL_FAILURE = 52
|
153
|
+
FEEDER_BLOCKED = 53
|
154
|
+
MAX_THERMOCOPLE_TEMP_EXCEEDED = 54
|
155
|
+
THERMOCOUPLE_WIRING_FAILURE = 55
|
156
|
+
UNKNOWN_ERROR = 255
|
110
157
|
|
111
158
|
|
112
159
|
@unique
|
pyplumio/devices/__init__.py
CHANGED
@@ -45,7 +45,7 @@ class Device(ABC, EventManager):
|
|
45
45
|
|
46
46
|
queue: asyncio.Queue[Frame]
|
47
47
|
|
48
|
-
def __init__(self, queue: asyncio.Queue[Frame]):
|
48
|
+
def __init__(self, queue: asyncio.Queue[Frame]) -> None:
|
49
49
|
"""Initialize a new device."""
|
50
50
|
super().__init__()
|
51
51
|
self.queue = queue
|
@@ -126,14 +126,14 @@ class PhysicalDevice(Device, ABC):
|
|
126
126
|
_network: NetworkInfo
|
127
127
|
_setup_frames: tuple[DataFrameDescription, ...]
|
128
128
|
|
129
|
-
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
|
129
|
+
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
130
130
|
"""Initialize a new physical device."""
|
131
131
|
super().__init__(queue)
|
132
132
|
self._network = network
|
133
133
|
|
134
134
|
def handle_frame(self, frame: Frame) -> None:
|
135
135
|
"""Handle frame received from the device."""
|
136
|
-
frame.
|
136
|
+
frame.assign_to(self)
|
137
137
|
if frame.data is not None:
|
138
138
|
for name, value in frame.data.items():
|
139
139
|
self.dispatch_nowait(name, value)
|
@@ -188,7 +188,7 @@ class VirtualDevice(Device, ABC):
|
|
188
188
|
|
189
189
|
def __init__(
|
190
190
|
self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
|
191
|
-
):
|
191
|
+
) -> None:
|
192
192
|
"""Initialize a new sub-device."""
|
193
193
|
super().__init__(queue)
|
194
194
|
self.parent = parent
|
pyplumio/devices/ecomax.py
CHANGED
@@ -110,7 +110,7 @@ class EcoMAX(PhysicalDevice):
|
|
110
110
|
_fuel_burned_timestamp_ns: int
|
111
111
|
_setup_frames = SETUP_FRAME_TYPES
|
112
112
|
|
113
|
-
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
|
113
|
+
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
|
114
114
|
"""Initialize a new ecoMAX controller."""
|
115
115
|
super().__init__(queue, network)
|
116
116
|
self._frame_versions = {}
|
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/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
|
|
@@ -60,12 +60,13 @@ class EcomaxParameter(Parameter):
|
|
60
60
|
"""Create a request to change the parameter."""
|
61
61
|
handler = partial(Request.create, recipient=self.device.address)
|
62
62
|
if self.description.name == ATTR_ECOMAX_CONTROL:
|
63
|
-
|
63
|
+
return await handler(
|
64
64
|
frame_type=FrameType.REQUEST_ECOMAX_CONTROL,
|
65
65
|
data={ATTR_VALUE: self.values.value},
|
66
66
|
)
|
67
|
-
|
68
|
-
|
67
|
+
|
68
|
+
if self.description.name == ATTR_THERMOSTAT_PROFILE:
|
69
|
+
return await handler(
|
69
70
|
frame_type=FrameType.REQUEST_SET_THERMOSTAT_PARAMETER,
|
70
71
|
data={
|
71
72
|
ATTR_INDEX: self._index,
|
@@ -74,13 +75,11 @@ class EcomaxParameter(Parameter):
|
|
74
75
|
ATTR_SIZE: 1,
|
75
76
|
},
|
76
77
|
)
|
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
78
|
|
83
|
-
return
|
79
|
+
return await handler(
|
80
|
+
frame_type=FrameType.REQUEST_SET_ECOMAX_PARAMETER,
|
81
|
+
data={ATTR_INDEX: self._index, ATTR_VALUE: self.values.value},
|
82
|
+
)
|
84
83
|
|
85
84
|
|
86
85
|
@dataslots
|
@@ -90,6 +89,7 @@ class EcomaxNumberDescription(EcomaxParameterDescription, NumberDescription):
|
|
90
89
|
|
91
90
|
multiplier: float = 1.0
|
92
91
|
offset: int = 0
|
92
|
+
precision: int = 6
|
93
93
|
|
94
94
|
|
95
95
|
class EcomaxNumber(EcomaxParameter, Number):
|
@@ -103,29 +103,27 @@ class EcomaxNumber(EcomaxParameter, Number):
|
|
103
103
|
self, value: float | int, retries: int = 5, timeout: float = 5.0
|
104
104
|
) -> bool:
|
105
105
|
"""Set a parameter value."""
|
106
|
-
value
|
106
|
+
value += self.description.offset
|
107
|
+
value = round(value / self.description.multiplier, self.description.precision)
|
107
108
|
return await super().set(value, retries, timeout)
|
108
109
|
|
109
110
|
@property
|
110
111
|
def value(self) -> float:
|
111
112
|
"""Return the value."""
|
112
|
-
|
113
|
-
|
114
|
-
) * self.description.multiplier
|
113
|
+
value = self.values.value - self.description.offset
|
114
|
+
return round(value * self.description.multiplier, self.description.precision)
|
115
115
|
|
116
116
|
@property
|
117
117
|
def min_value(self) -> float:
|
118
118
|
"""Return the minimum allowed value."""
|
119
|
-
|
120
|
-
|
121
|
-
) * self.description.multiplier
|
119
|
+
value = self.values.min_value - self.description.offset
|
120
|
+
return round(value * self.description.multiplier, self.description.precision)
|
122
121
|
|
123
122
|
@property
|
124
123
|
def max_value(self) -> float:
|
125
124
|
"""Return the maximum allowed value."""
|
126
|
-
|
127
|
-
|
128
|
-
) * self.description.multiplier
|
125
|
+
value = self.values.max_value - self.description.offset
|
126
|
+
return round(value * self.description.multiplier, self.description.precision)
|
129
127
|
|
130
128
|
|
131
129
|
@dataslots
|
@@ -292,11 +290,11 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
|
|
292
290
|
unit_of_measurement=UnitOfMeasurement.CELSIUS,
|
293
291
|
),
|
294
292
|
EcomaxNumberDescription(
|
295
|
-
name="
|
293
|
+
name="grate_fan_work",
|
296
294
|
unit_of_measurement=UnitOfMeasurement.SECONDS,
|
297
295
|
),
|
298
296
|
EcomaxNumberDescription(
|
299
|
-
name="
|
297
|
+
name="grate_fan_pause",
|
300
298
|
unit_of_measurement=UnitOfMeasurement.MINUTES,
|
301
299
|
),
|
302
300
|
EcomaxNumberDescription(
|
@@ -416,10 +414,10 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
|
|
416
414
|
unit_of_measurement=PERCENTAGE,
|
417
415
|
),
|
418
416
|
EcomaxNumberDescription(
|
419
|
-
name="
|
417
|
+
name="burning_off_fan_work",
|
420
418
|
),
|
421
419
|
EcomaxNumberDescription(
|
422
|
-
name="
|
420
|
+
name="burning_off_fan_pause",
|
423
421
|
),
|
424
422
|
EcomaxNumberDescription(
|
425
423
|
name="start_burning_off",
|
@@ -73,6 +73,7 @@ class MixerNumberDescription(MixerParameterDescription, NumberDescription):
|
|
73
73
|
|
74
74
|
multiplier: float = 1.0
|
75
75
|
offset: int = 0
|
76
|
+
precision: int = 6
|
76
77
|
|
77
78
|
|
78
79
|
class MixerNumber(MixerParameter, Number):
|
@@ -86,29 +87,27 @@ class MixerNumber(MixerParameter, Number):
|
|
86
87
|
self, value: int | float, retries: int = 5, timeout: float = 5.0
|
87
88
|
) -> bool:
|
88
89
|
"""Set a parameter value."""
|
89
|
-
value
|
90
|
+
value += self.description.offset
|
91
|
+
value = round(value / self.description.multiplier, self.description.precision)
|
90
92
|
return await super().set(value, retries, timeout)
|
91
93
|
|
92
94
|
@property
|
93
95
|
def value(self) -> float:
|
94
96
|
"""Return the parameter value."""
|
95
|
-
|
96
|
-
|
97
|
-
) * self.description.multiplier
|
97
|
+
value = self.values.value - self.description.offset
|
98
|
+
return round(value * self.description.multiplier, self.description.precision)
|
98
99
|
|
99
100
|
@property
|
100
101
|
def min_value(self) -> float:
|
101
102
|
"""Return the minimum allowed value."""
|
102
|
-
|
103
|
-
|
104
|
-
) * self.description.multiplier
|
103
|
+
value = self.values.min_value - self.description.offset
|
104
|
+
return round(value * self.description.multiplier, self.description.precision)
|
105
105
|
|
106
106
|
@property
|
107
107
|
def max_value(self) -> float:
|
108
108
|
"""Return the maximum allowed value."""
|
109
|
-
|
110
|
-
|
111
|
-
) * self.description.multiplier
|
109
|
+
value = self.values.max_value - self.description.offset
|
110
|
+
return round(value * self.description.multiplier, self.description.precision)
|
112
111
|
|
113
112
|
|
114
113
|
@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
|
@@ -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)
|
@@ -93,6 +93,7 @@ class ThermostatNumberDescription(ThermostatParameterDescription, NumberDescript
|
|
93
93
|
"""Represent a thermostat number description."""
|
94
94
|
|
95
95
|
multiplier: float = 1.0
|
96
|
+
precision: int = 6
|
96
97
|
|
97
98
|
|
98
99
|
class ThermostatNumber(ThermostatParameter, Number):
|
@@ -106,23 +107,31 @@ class ThermostatNumber(ThermostatParameter, Number):
|
|
106
107
|
self, value: int | float, retries: int = 5, timeout: float = 5.0
|
107
108
|
) -> bool:
|
108
109
|
"""Set a parameter value."""
|
109
|
-
value = value / self.description.multiplier
|
110
|
+
value = round(value / self.description.multiplier, self.description.precision)
|
110
111
|
return await super().set(value, retries, timeout)
|
111
112
|
|
112
113
|
@property
|
113
114
|
def value(self) -> float:
|
114
115
|
"""Return the value."""
|
115
|
-
return
|
116
|
+
return round(
|
117
|
+
self.values.value * self.description.multiplier, self.description.precision
|
118
|
+
)
|
116
119
|
|
117
120
|
@property
|
118
121
|
def min_value(self) -> float:
|
119
122
|
"""Return the minimum allowed value."""
|
120
|
-
return
|
123
|
+
return round(
|
124
|
+
self.values.min_value * self.description.multiplier,
|
125
|
+
self.description.precision,
|
126
|
+
)
|
121
127
|
|
122
128
|
@property
|
123
129
|
def max_value(self) -> float:
|
124
130
|
"""Return the maximum allowed value."""
|
125
|
-
return
|
131
|
+
return round(
|
132
|
+
self.values.max_value * self.description.multiplier,
|
133
|
+
self.description.precision,
|
134
|
+
)
|
126
135
|
|
127
136
|
|
128
137
|
@dataslots
|
@@ -247,7 +256,7 @@ class ThermostatParametersStructure(StructureDecoder):
|
|
247
256
|
self, message: bytearray, offset: int = 0, data: dict[str, Any] | None = None
|
248
257
|
) -> tuple[dict[str, Any], int]:
|
249
258
|
"""Decode bytes and return message data and offset."""
|
250
|
-
if (device := self.frame.
|
259
|
+
if (device := self.frame.handler) is not None and (
|
251
260
|
thermostats := device.get_nowait(ATTR_THERMOSTATS_AVAILABLE, 0)
|
252
261
|
) == 0:
|
253
262
|
return (
|
File without changes
|
File without changes
|