PyPlumIO 0.4.0.post1__py3-none-any.whl → 0.4.2__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.4.0.post1.dist-info → PyPlumIO-0.4.2.dist-info}/METADATA +1 -1
- {PyPlumIO-0.4.0.post1.dist-info → PyPlumIO-0.4.2.dist-info}/RECORD +14 -14
- pyplumio/_version.py +2 -2
- pyplumio/const.py +3 -0
- pyplumio/devices/__init__.py +19 -18
- pyplumio/devices/ecomax.py +16 -3
- pyplumio/filters.py +63 -27
- pyplumio/helpers/typing.py +9 -1
- pyplumio/structures/ecomax_parameters.py +25 -12
- pyplumio/structures/mixer_parameters.py +28 -34
- pyplumio/structures/thermostat_parameters.py +49 -51
- {PyPlumIO-0.4.0.post1.dist-info → PyPlumIO-0.4.2.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.4.0.post1.dist-info → PyPlumIO-0.4.2.dist-info}/WHEEL +0 -0
- {PyPlumIO-0.4.0.post1.dist-info → PyPlumIO-0.4.2.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,15 @@
|
|
1
1
|
pyplumio/__init__.py,sha256=HZuhLA98rflVEU5cc2-UpboDSJUW1e6Eocfdok9MHZM,1273
|
2
2
|
pyplumio/__main__.py,sha256=j8fh3P6yGwB1qumdUlW8X22-DOwisdo4IcUEmlYPGbM,497
|
3
|
-
pyplumio/_version.py,sha256=
|
3
|
+
pyplumio/_version.py,sha256=MHz4IpMPBfgMjF4EUb11AphGoLXSC-lOtHXxzKzJNjk,160
|
4
4
|
pyplumio/connection.py,sha256=ABeJFbnqTq89i5J1TRR2sdQeRrq1Pewdi_TaDjzSOOI,5494
|
5
|
-
pyplumio/const.py,sha256=
|
5
|
+
pyplumio/const.py,sha256=qCsgmv-uIjLBae46AlMMPPHxag301LJaRTUo9NLmU2M,3039
|
6
6
|
pyplumio/exceptions.py,sha256=cpLGRl9db6cU1Z6WXhQ7yl1V7Te193QymWiOoRF8KAI,798
|
7
|
-
pyplumio/filters.py,sha256=
|
7
|
+
pyplumio/filters.py,sha256=MM3l67Iq0Xm2aZRPkAOAxoeaMT3cTsF-vL95Sid4GTM,6548
|
8
8
|
pyplumio/protocol.py,sha256=h90uhCcqscukJ0GVX6rYv_LH5UsE2om87ZXo3EWZ77s,6223
|
9
9
|
pyplumio/stream.py,sha256=n1dae3oKnnU0XWsVgzVszXPdVXkYChdw-bUTKcHr05s,3738
|
10
10
|
pyplumio/util.py,sha256=oKKO9eJmrmXf0XHZG5P4fGR_R1jMSU_BAXU82ZnDS8Y,2603
|
11
|
-
pyplumio/devices/__init__.py,sha256=
|
12
|
-
pyplumio/devices/ecomax.py,sha256=
|
11
|
+
pyplumio/devices/__init__.py,sha256=DIT3wV0BDU8y1zaM5j9EAhNd5MbTsuCElLZBvZ-3GH8,4871
|
12
|
+
pyplumio/devices/ecomax.py,sha256=L74fUjFE3q4lsu4CccsMxCFenljmY0sF1D1CYLoRBDc,15223
|
13
13
|
pyplumio/devices/ecoster.py,sha256=IqyyLDqasDSK9taFGlxDoX8Dl8fYOKDL00RB4zX-tZs,314
|
14
14
|
pyplumio/devices/mixer.py,sha256=iODTCYHt29XNCam0hnGMdP8VN1uS24b489KD5-9M6-M,1919
|
15
15
|
pyplumio/devices/thermostat.py,sha256=J8Iq5jfRmvoFQH2nU-mXNJeGMekp4lqJhtdS2qTVKRM,1701
|
@@ -25,19 +25,19 @@ pyplumio/helpers/parameter.py,sha256=4PxOQg0jFAPwrdQkwLXjaey-CgDCGsNCcglldk2zwm4
|
|
25
25
|
pyplumio/helpers/schedule.py,sha256=pbKRqcU48IRrEKZQOncBQkz_WExxjtDDHZ3x-MHpVIg,4077
|
26
26
|
pyplumio/helpers/task_manager.py,sha256=fhqbSNaInoWXL3YjkFe_sCgh8suOwRnufqWBrXIgPeM,1026
|
27
27
|
pyplumio/helpers/timeout.py,sha256=az12syvMGhTxzeiLGZIcJYG6MI4t6LCZeZyt6viRh04,846
|
28
|
-
pyplumio/helpers/typing.py,sha256=
|
28
|
+
pyplumio/helpers/typing.py,sha256=wDy-TLxLAF_jrw8QW5g4EZpbV8unRYqH_VO8s6VnIYE,557
|
29
29
|
pyplumio/helpers/uid.py,sha256=Gt1l9gs-RMummmpfu5IoRokGurxRzJeWZrfwiyeGR9c,1044
|
30
30
|
pyplumio/structures/__init__.py,sha256=imqgigigBkwDpHKLwiLTrAh0vqaYcvnm8Y5676uT07Y,1482
|
31
31
|
pyplumio/structures/alerts.py,sha256=_AIsA_Uvp2xm8JiQ7a3jt7VuZWcFiNHYtZTOHBAFIkQ,2644
|
32
32
|
pyplumio/structures/data_schema.py,sha256=wOWEtoqjtn2qq-Gxb-HParxTnmAjsOjIBxp211Qn66I,1174
|
33
|
-
pyplumio/structures/ecomax_parameters.py,sha256=
|
33
|
+
pyplumio/structures/ecomax_parameters.py,sha256=8y1ZFN9NKoiKt_zxW-_m6QBsWssQ2A0uK6oQv6E4LcA,16082
|
34
34
|
pyplumio/structures/fan_power.py,sha256=nlurG1-sPyUQbasuxZChwDW4ml3Mp_Jc5RkUpv5NDzg,859
|
35
35
|
pyplumio/structures/frame_versions.py,sha256=LqqlC7CVyRDfBMw1Y2Tt0ZaDdlj1K3nlWbb3C3LwzD4,1183
|
36
36
|
pyplumio/structures/fuel_consumption.py,sha256=LxQf0Vg7-XnikD0otXvWep6nIP8mK3j_P4cB2xjWwSk,961
|
37
37
|
pyplumio/structures/fuel_level.py,sha256=_u6WMLpnIl0W3jA1QKYG708s5PdaaIM35yeEZZ8Srik,800
|
38
38
|
pyplumio/structures/lambda_sensor.py,sha256=gIsonwMOtKXhp_v55yKxUm1XNeUGvRkHUI0JwDSOAsY,1315
|
39
39
|
pyplumio/structures/load.py,sha256=wltZnHmEE4yk9DzYpwPRs4a8Z8YS7IhBqG81Kpe5c-s,772
|
40
|
-
pyplumio/structures/mixer_parameters.py,sha256=
|
40
|
+
pyplumio/structures/mixer_parameters.py,sha256=gShc2ST0IQcfHFhmKVgkz8s1DUdqgVBwIZl_PSXJry4,6115
|
41
41
|
pyplumio/structures/mixer_sensors.py,sha256=4AEFPlV-seESz4DDNn6kUl5OJWCaXRejEAtSX2_JdvU,1729
|
42
42
|
pyplumio/structures/modules.py,sha256=jFO723D-40Ac1rqchDj7ULWARx-nnx_3i9Tjnn_Llxo,2225
|
43
43
|
pyplumio/structures/network_info.py,sha256=-Pz9nuAsTA2vcwNaBkgkvsTfavpikctYiUmrMa6EjCc,3469
|
@@ -51,10 +51,10 @@ pyplumio/structures/regulator_data.py,sha256=GOfnFcoLN-3vhSIg4GLRDh_5uEWZHYk9E5Y
|
|
51
51
|
pyplumio/structures/schedules.py,sha256=YFDHYaiygc60mQm9EzrXXnMic9EKQsi-CwDM1jBECUQ,6135
|
52
52
|
pyplumio/structures/statuses.py,sha256=BeSOabFIewiUBxAXgsuxzjRi4Zl-_O2qDbn61w7DtUU,1036
|
53
53
|
pyplumio/structures/temperatures.py,sha256=lPRYCCciaGTBU_ShWwz_bWHvsfIRL3n16CnTbZK9iC4,2298
|
54
|
-
pyplumio/structures/thermostat_parameters.py,sha256=
|
54
|
+
pyplumio/structures/thermostat_parameters.py,sha256=M-VpUb7z-0rsES-PqUKpdmWP8TcV9IACkS6yPx4ivqo,6399
|
55
55
|
pyplumio/structures/thermostat_sensors.py,sha256=yU5F2KXnpOMyHSdAfoIY6JmEIlSYMf05qmyvN0F4A5c,2236
|
56
|
-
PyPlumIO-0.4.
|
57
|
-
PyPlumIO-0.4.
|
58
|
-
PyPlumIO-0.4.
|
59
|
-
PyPlumIO-0.4.
|
60
|
-
PyPlumIO-0.4.
|
56
|
+
PyPlumIO-0.4.2.dist-info/LICENSE,sha256=g56wJgobsehVtkJD8MhM6isAnsdOUmRPv7fIaXI4Joo,1067
|
57
|
+
PyPlumIO-0.4.2.dist-info/METADATA,sha256=8iQtMCrvNMnHyiN7p0q3XjcSR6sbljW4FEQJ3rjF0Eo,4792
|
58
|
+
PyPlumIO-0.4.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
59
|
+
PyPlumIO-0.4.2.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
60
|
+
PyPlumIO-0.4.2.dist-info/RECORD,,
|
pyplumio/_version.py
CHANGED
pyplumio/const.py
CHANGED
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
4
4
|
from enum import IntEnum, unique
|
5
5
|
from typing import Final
|
6
6
|
|
7
|
+
UNDEFINED: Final = "undefined"
|
8
|
+
|
7
9
|
# Binary states.
|
8
10
|
STATE_ON: Final = "on"
|
9
11
|
STATE_OFF: Final = "off"
|
@@ -12,6 +14,7 @@ STATE_OFF: Final = "off"
|
|
12
14
|
ATTR_CONNECTED: Final = "connected"
|
13
15
|
ATTR_CURRENT_TEMP: Final = "current_temp"
|
14
16
|
ATTR_DEVICE_INDEX: Final = "device_index"
|
17
|
+
ATTR_FRAME_ERRORS: Final = "frame_errors"
|
15
18
|
ATTR_INDEX: Final = "index"
|
16
19
|
ATTR_LOADED: Final = "loaded"
|
17
20
|
ATTR_OFFSET: Final = "offset"
|
pyplumio/devices/__init__.py
CHANGED
@@ -6,7 +6,7 @@ import logging
|
|
6
6
|
from typing import ClassVar
|
7
7
|
|
8
8
|
from pyplumio import util
|
9
|
-
from pyplumio.const import ATTR_LOADED, DeviceType, FrameType
|
9
|
+
from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType
|
10
10
|
from pyplumio.exceptions import ParameterNotFoundError, UnknownDeviceError
|
11
11
|
from pyplumio.frames import DataFrameDescription, Frame, Request, get_frame_handler
|
12
12
|
from pyplumio.helpers.event_manager import EventManager
|
@@ -99,22 +99,23 @@ class Addressable(Device):
|
|
99
99
|
|
100
100
|
async def async_setup(self) -> bool:
|
101
101
|
"""Setup addressable device object."""
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
self.
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
102
|
+
results = await asyncio.gather(
|
103
|
+
*{
|
104
|
+
self.create_task(
|
105
|
+
self.request(description.provides, description.frame_type)
|
106
|
+
)
|
107
|
+
for description in self._frame_types
|
108
|
+
},
|
109
|
+
return_exceptions=True,
|
110
|
+
)
|
111
|
+
|
112
|
+
errors = [
|
113
|
+
result.args[1] for result in results if isinstance(result, ValueError)
|
114
|
+
]
|
115
|
+
|
116
|
+
await self.dispatch(ATTR_FRAME_ERRORS, errors)
|
117
|
+
await self.dispatch(ATTR_LOADED, True)
|
118
|
+
return True
|
118
119
|
|
119
120
|
async def request(
|
120
121
|
self,
|
@@ -136,7 +137,7 @@ class Addressable(Device):
|
|
136
137
|
except asyncio.TimeoutError:
|
137
138
|
retries -= 1
|
138
139
|
|
139
|
-
raise ValueError(f'could not request "{name}"
|
140
|
+
raise ValueError(f'could not request "{name}"', frame_type)
|
140
141
|
|
141
142
|
|
142
143
|
class SubDevice(Device):
|
pyplumio/devices/ecomax.py
CHANGED
@@ -8,6 +8,7 @@ import time
|
|
8
8
|
from typing import ClassVar, Final
|
9
9
|
|
10
10
|
from pyplumio.const import (
|
11
|
+
ATTR_FRAME_ERRORS,
|
11
12
|
ATTR_PASSWORD,
|
12
13
|
ATTR_SENSORS,
|
13
14
|
ATTR_STATE,
|
@@ -140,12 +141,24 @@ class EcoMAX(Addressable):
|
|
140
141
|
|
141
142
|
super().handle_frame(frame)
|
142
143
|
|
144
|
+
def _has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
|
145
|
+
"""Check if device instance has a version of the frame."""
|
146
|
+
return (
|
147
|
+
frame_type in self._frame_versions
|
148
|
+
and self._frame_versions[frame_type] == version
|
149
|
+
)
|
150
|
+
|
151
|
+
def _frame_is_supported(self, frame_type: FrameType | int) -> bool:
|
152
|
+
"""Check if frame is supported by the device."""
|
153
|
+
return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])
|
154
|
+
|
143
155
|
async def _update_frame_versions(self, versions: dict[int, int]) -> None:
|
144
156
|
"""Check versions and fetch outdated frames."""
|
145
157
|
for frame_type, version in versions.items():
|
146
|
-
if
|
147
|
-
frame_type
|
148
|
-
|
158
|
+
if (
|
159
|
+
is_known_frame_type(frame_type)
|
160
|
+
and self._frame_is_supported(frame_type)
|
161
|
+
and not self._has_frame_version(frame_type, version)
|
149
162
|
):
|
150
163
|
# We don't have this frame or it's version has changed.
|
151
164
|
request = factory(get_frame_handler(frame_type), recipient=self.address)
|
pyplumio/filters.py
CHANGED
@@ -4,34 +4,70 @@ from __future__ import annotations
|
|
4
4
|
from abc import ABC, abstractmethod
|
5
5
|
import math
|
6
6
|
import time
|
7
|
-
from typing import Any, Final
|
7
|
+
from typing import Any, Final, SupportsFloat, SupportsIndex, overload
|
8
8
|
|
9
|
+
from pyplumio.const import UNDEFINED
|
9
10
|
from pyplumio.helpers.parameter import Parameter
|
10
|
-
from pyplumio.helpers.typing import EventCallbackType
|
11
|
+
from pyplumio.helpers.typing import EventCallbackType, SupportsSubtraction
|
11
12
|
|
12
13
|
TOLERANCE: Final = 0.1
|
13
14
|
|
14
15
|
|
15
|
-
|
16
|
+
@overload
|
17
|
+
def _significantly_changed(old: Parameter, new: Parameter) -> bool:
|
18
|
+
"""Check if parameter is significantly changed."""
|
19
|
+
|
20
|
+
|
21
|
+
@overload
|
22
|
+
def _significantly_changed(
|
23
|
+
old: SupportsFloat | SupportsIndex, new: SupportsFloat | SupportsIndex
|
24
|
+
) -> bool:
|
25
|
+
"""Check if float value is significantly changed."""
|
26
|
+
|
27
|
+
|
28
|
+
def _significantly_changed(old, new) -> bool:
|
16
29
|
"""Check if value is significantly changed."""
|
17
|
-
if
|
30
|
+
if old == UNDEFINED:
|
18
31
|
return True
|
19
32
|
|
33
|
+
if isinstance(old, Parameter) and old.is_changed:
|
34
|
+
return True
|
35
|
+
|
36
|
+
if isinstance(old, Parameter) and isinstance(new, Parameter):
|
37
|
+
return (
|
38
|
+
old.value != new.value
|
39
|
+
or old.min_value != new.min_value
|
40
|
+
or old.max_value != new.max_value
|
41
|
+
)
|
42
|
+
|
20
43
|
try:
|
21
|
-
return not math.isclose(
|
44
|
+
return not math.isclose(old, new, abs_tol=TOLERANCE)
|
22
45
|
except TypeError:
|
23
46
|
pass
|
24
47
|
|
25
|
-
return
|
48
|
+
return old != new
|
49
|
+
|
50
|
+
|
51
|
+
@overload
|
52
|
+
def _diffence_between(old: list, new: list) -> list:
|
53
|
+
"""Return the difference between lists."""
|
26
54
|
|
27
55
|
|
28
|
-
|
56
|
+
@overload
|
57
|
+
def _diffence_between(old: SupportsSubtraction, new: SupportsSubtraction) -> list:
|
58
|
+
"""Return the difference between lists."""
|
59
|
+
|
60
|
+
|
61
|
+
def _diffence_between(old, new):
|
29
62
|
"""Return the difference between values."""
|
30
|
-
if
|
31
|
-
return
|
63
|
+
if old == UNDEFINED:
|
64
|
+
return None
|
65
|
+
|
66
|
+
if isinstance(old, list) and isinstance(new, list):
|
67
|
+
return [x for x in new if x not in old]
|
32
68
|
|
33
|
-
if hasattr(
|
34
|
-
return
|
69
|
+
if hasattr(old, "__sub__") and hasattr(new, "__sub__"):
|
70
|
+
return new - old
|
35
71
|
|
36
72
|
return None
|
37
73
|
|
@@ -40,12 +76,12 @@ class Filter(ABC):
|
|
40
76
|
"""Represents base for value callback modifiers."""
|
41
77
|
|
42
78
|
_callback: Any
|
43
|
-
_value: Any
|
79
|
+
_value: Any = UNDEFINED
|
44
80
|
|
45
81
|
def __init__(self, callback: EventCallbackType):
|
46
82
|
"""Initialize new Filter object."""
|
47
83
|
self._callback = callback
|
48
|
-
self._value =
|
84
|
+
self._value = UNDEFINED
|
49
85
|
|
50
86
|
def __eq__(self, other) -> bool:
|
51
87
|
"""Compare debounced callbacks."""
|
@@ -59,21 +95,21 @@ class Filter(ABC):
|
|
59
95
|
|
60
96
|
@abstractmethod
|
61
97
|
async def __call__(self, new_value):
|
62
|
-
"""Set new value for the callback."""
|
98
|
+
"""Set a new value for the callback."""
|
63
99
|
|
64
100
|
|
65
101
|
class _OnChange(Filter):
|
66
102
|
"""Provides changed functionality to the callback."""
|
67
103
|
|
68
104
|
async def __call__(self, new_value):
|
69
|
-
"""Set new value for the callback."""
|
105
|
+
"""Set a new value for the callback."""
|
70
106
|
if _significantly_changed(self._value, new_value):
|
71
107
|
self._value = new_value
|
72
108
|
return await self._callback(new_value)
|
73
109
|
|
74
110
|
|
75
111
|
def on_change(callback: EventCallbackType) -> _OnChange:
|
76
|
-
"""Helper for change callback filter."""
|
112
|
+
"""Helper for a change callback filter."""
|
77
113
|
return _OnChange(callback)
|
78
114
|
|
79
115
|
|
@@ -90,20 +126,20 @@ class _Debounce(Filter):
|
|
90
126
|
self._min_calls = min_calls
|
91
127
|
|
92
128
|
async def __call__(self, new_value):
|
93
|
-
"""Set new value for the callback."""
|
129
|
+
"""Set a new value for the callback."""
|
94
130
|
if _significantly_changed(self._value, new_value):
|
95
131
|
self._calls += 1
|
96
132
|
else:
|
97
133
|
self._calls = 0
|
98
134
|
|
99
|
-
if self.
|
135
|
+
if self._value == UNDEFINED or self._calls >= self._min_calls:
|
100
136
|
self._value = new_value
|
101
137
|
self._calls = 0
|
102
138
|
return await self._callback(new_value)
|
103
139
|
|
104
140
|
|
105
141
|
def debounce(callback: EventCallbackType, min_calls) -> _Debounce:
|
106
|
-
"""Helper method for debounce callback filter."""
|
142
|
+
"""Helper method for a debounce callback filter."""
|
107
143
|
return _Debounce(callback, min_calls)
|
108
144
|
|
109
145
|
|
@@ -120,7 +156,7 @@ class _Throttle(Filter):
|
|
120
156
|
self._timeout = seconds
|
121
157
|
|
122
158
|
async def __call__(self, new_value):
|
123
|
-
"""
|
159
|
+
"""Set a new value for the callback."""
|
124
160
|
current_timestamp = time.monotonic()
|
125
161
|
if (
|
126
162
|
self._last_called is None
|
@@ -131,7 +167,7 @@ class _Throttle(Filter):
|
|
131
167
|
|
132
168
|
|
133
169
|
def throttle(callback: EventCallbackType, seconds: float) -> _Throttle:
|
134
|
-
"""Helper method for throttle callback filter."""
|
170
|
+
"""Helper method for a throttle callback filter."""
|
135
171
|
return _Throttle(callback, seconds)
|
136
172
|
|
137
173
|
|
@@ -139,16 +175,16 @@ class _Delta(Filter):
|
|
139
175
|
"""Provides ability to pass call difference to the callback."""
|
140
176
|
|
141
177
|
async def __call__(self, new_value):
|
142
|
-
"""
|
143
|
-
|
144
|
-
|
178
|
+
"""Set new value for the callback."""
|
179
|
+
if _significantly_changed(self._value, new_value):
|
180
|
+
old_value = self._value
|
145
181
|
self._value = new_value
|
146
182
|
if (difference := _diffence_between(old_value, new_value)) is not None:
|
147
183
|
return await self._callback(difference)
|
148
184
|
|
149
185
|
|
150
186
|
def delta(callback: EventCallbackType) -> _Delta:
|
151
|
-
"""Helper method for delta callback filter."""
|
187
|
+
"""Helper method for a delta callback filter."""
|
152
188
|
return _Delta(callback)
|
153
189
|
|
154
190
|
|
@@ -168,7 +204,7 @@ class _Aggregate(Filter):
|
|
168
204
|
self._sum = 0.0
|
169
205
|
|
170
206
|
async def __call__(self, new_value):
|
171
|
-
"""Set new value for the callback."""
|
207
|
+
"""Set a new value for the callback."""
|
172
208
|
current_timestamp = time.monotonic()
|
173
209
|
try:
|
174
210
|
self._sum += new_value
|
@@ -185,5 +221,5 @@ class _Aggregate(Filter):
|
|
185
221
|
|
186
222
|
|
187
223
|
def aggregate(callback: EventCallbackType, seconds: float) -> _Aggregate:
|
188
|
-
"""Helper method for total callback filter."""
|
224
|
+
"""Helper method for a total callback filter."""
|
189
225
|
return _Aggregate(callback, seconds)
|
pyplumio/helpers/typing.py
CHANGED
@@ -2,9 +2,17 @@
|
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
4
|
from collections.abc import Awaitable, Callable
|
5
|
-
from typing import Any, Literal, Union
|
5
|
+
from typing import Any, Literal, Protocol, Union
|
6
6
|
|
7
7
|
ParameterDataType = tuple[int, int, int]
|
8
8
|
ParameterValueType = Union[int, float, bool, Literal["off"], Literal["on"]]
|
9
9
|
EventDataType = dict[Union[str, int], Any]
|
10
10
|
EventCallbackType = Callable[[Any], Awaitable[Any]]
|
11
|
+
UndefinedType = Literal["undefined"]
|
12
|
+
|
13
|
+
|
14
|
+
class SupportsSubtraction(Protocol):
|
15
|
+
"""Supports subtraction operation."""
|
16
|
+
|
17
|
+
def __sub__(self, other):
|
18
|
+
"""Subtracts a value."""
|
@@ -10,13 +10,15 @@ from pyplumio.devices import Addressable
|
|
10
10
|
from pyplumio.frames import Request
|
11
11
|
from pyplumio.helpers.factory import factory
|
12
12
|
from pyplumio.helpers.parameter import BinaryParameter, Parameter, ParameterDescription
|
13
|
-
from pyplumio.helpers.typing import EventDataType,
|
13
|
+
from pyplumio.helpers.typing import EventDataType, ParameterValueType
|
14
14
|
from pyplumio.structures import StructureDecoder, ensure_device_data
|
15
15
|
from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PROFILE
|
16
16
|
|
17
17
|
ATTR_ECOMAX_CONTROL: Final = "ecomax_control"
|
18
18
|
ATTR_ECOMAX_PARAMETERS: Final = "ecomax_parameters"
|
19
19
|
|
20
|
+
ECOMAX_PARAMETER_SIZE: Final = 3
|
21
|
+
|
20
22
|
|
21
23
|
class EcomaxParameter(Parameter):
|
22
24
|
"""Represents ecoMAX parameter."""
|
@@ -301,22 +303,33 @@ THERMOSTAT_PROFILE_PARAMETER = EcomaxParameterDescription(name=ATTR_THERMOSTAT_P
|
|
301
303
|
class EcomaxParametersStructure(StructureDecoder):
|
302
304
|
"""Represents ecoMAX parameters data structure."""
|
303
305
|
|
306
|
+
_offset: int
|
307
|
+
|
308
|
+
def _ecomax_parameter(self, message: bytearray, start: int, end: int):
|
309
|
+
"""Yields ecoMAX parameters."""
|
310
|
+
for index in range(start, start + end):
|
311
|
+
if parameter := util.unpack_parameter(message, self._offset):
|
312
|
+
yield (index, parameter)
|
313
|
+
|
314
|
+
self._offset += ECOMAX_PARAMETER_SIZE
|
315
|
+
|
304
316
|
def decode(
|
305
317
|
self, message: bytearray, offset: int = 0, data: EventDataType | None = None
|
306
318
|
) -> tuple[EventDataType, int]:
|
307
319
|
"""Decode bytes and return message data and offset."""
|
308
|
-
first_index = message[offset + 1]
|
309
|
-
last_index = message[offset + 2]
|
310
|
-
offset += 3
|
311
|
-
ecomax_parameters: list[tuple[int, ParameterDataType]] = []
|
312
|
-
for index in range(first_index, first_index + last_index):
|
313
|
-
parameter = util.unpack_parameter(message, offset)
|
314
|
-
if parameter is not None:
|
315
|
-
ecomax_parameters.append((index, parameter))
|
316
320
|
|
317
|
-
|
321
|
+
start = message[offset + 1]
|
322
|
+
end = message[offset + 2]
|
323
|
+
self._offset = offset + 3
|
318
324
|
|
319
325
|
return (
|
320
|
-
ensure_device_data(
|
321
|
-
|
326
|
+
ensure_device_data(
|
327
|
+
data,
|
328
|
+
{
|
329
|
+
ATTR_ECOMAX_PARAMETERS: list(
|
330
|
+
self._ecomax_parameter(message, start, end)
|
331
|
+
)
|
332
|
+
},
|
333
|
+
),
|
334
|
+
self._offset,
|
322
335
|
)
|
@@ -1,7 +1,6 @@
|
|
1
1
|
"""Contains mixer parameter structure decoder."""
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
|
-
from collections.abc import Iterable
|
5
4
|
from dataclasses import dataclass
|
6
5
|
from typing import TYPE_CHECKING, Final
|
7
6
|
|
@@ -18,6 +17,8 @@ if TYPE_CHECKING:
|
|
18
17
|
|
19
18
|
ATTR_MIXER_PARAMETERS: Final = "mixer_parameters"
|
20
19
|
|
20
|
+
MIXER_PARAMETER_SIZE: Final = 3
|
21
|
+
|
21
22
|
|
22
23
|
class MixerParameter(Parameter):
|
23
24
|
"""Represents mixer parameter."""
|
@@ -117,48 +118,41 @@ ECOMAX_I_MIXER_PARAMETERS: tuple[MixerParameterDescription, ...] = (
|
|
117
118
|
)
|
118
119
|
|
119
120
|
|
120
|
-
|
121
|
-
|
122
|
-
) -> tuple[list[tuple[int, ParameterDataType]], int]:
|
123
|
-
"""Decode parameters for a single mixer."""
|
124
|
-
parameters: list[tuple[int, ParameterDataType]] = []
|
125
|
-
for index in indexes:
|
126
|
-
parameter = util.unpack_parameter(message, offset)
|
127
|
-
if parameter is not None:
|
128
|
-
parameters.append((index, parameter))
|
121
|
+
class MixerParametersStructure(StructureDecoder):
|
122
|
+
"""Represent mixer parameters data structure."""
|
129
123
|
|
130
|
-
|
124
|
+
_offset: int
|
131
125
|
|
132
|
-
|
126
|
+
def _mixer_parameter(self, message: bytearray, start: int, end: int):
|
127
|
+
"""Yields mixer parameters."""
|
128
|
+
for index in range(start, start + end):
|
129
|
+
if (parameter := util.unpack_parameter(message, self._offset)) is not None:
|
130
|
+
yield (index, parameter)
|
133
131
|
|
134
|
-
|
135
|
-
class MixerParametersStructure(StructureDecoder):
|
136
|
-
"""Represent mixer parameters data structure."""
|
132
|
+
self._offset += MIXER_PARAMETER_SIZE
|
137
133
|
|
138
134
|
def decode(
|
139
135
|
self, message: bytearray, offset: int = 0, data: EventDataType | None = None
|
140
136
|
) -> tuple[EventDataType, int]:
|
141
137
|
"""Decode bytes and return message data and offset."""
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
138
|
+
start = message[offset + 1]
|
139
|
+
end = message[offset + 2]
|
140
|
+
mixers = message[offset + 3]
|
141
|
+
self._offset = offset + 4
|
142
|
+
|
147
143
|
mixer_parameters: dict[int, list[tuple[int, ParameterDataType]]] = {}
|
148
|
-
for
|
149
|
-
parameters,
|
150
|
-
|
151
|
-
offset,
|
152
|
-
range(first_index, parameter_count_per_mixer),
|
153
|
-
)
|
154
|
-
if parameters:
|
155
|
-
mixer_parameters[index] = parameters
|
156
|
-
|
157
|
-
if not mixer_parameters:
|
158
|
-
# No mixer parameters detected.
|
159
|
-
return ensure_device_data(data, {ATTR_MIXER_PARAMETERS: None}), offset
|
144
|
+
for mixer in range(mixers):
|
145
|
+
if parameters := list(self._mixer_parameter(message, start, end)):
|
146
|
+
mixer_parameters[mixer] = parameters
|
160
147
|
|
161
148
|
return (
|
162
|
-
ensure_device_data(
|
163
|
-
|
149
|
+
ensure_device_data(
|
150
|
+
data,
|
151
|
+
{
|
152
|
+
ATTR_MIXER_PARAMETERS: (
|
153
|
+
None if not mixer_parameters else mixer_parameters
|
154
|
+
)
|
155
|
+
},
|
156
|
+
),
|
157
|
+
self._offset,
|
164
158
|
)
|
@@ -1,7 +1,6 @@
|
|
1
1
|
"""Contains thermostat parameter structure decoder."""
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
|
-
from collections.abc import Iterable
|
5
4
|
from dataclasses import dataclass
|
6
5
|
from typing import TYPE_CHECKING, Final
|
7
6
|
|
@@ -14,12 +13,15 @@ from pyplumio.helpers.typing import EventDataType, ParameterDataType, ParameterV
|
|
14
13
|
from pyplumio.structures import StructureDecoder, ensure_device_data
|
15
14
|
from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_COUNT
|
16
15
|
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from pyplumio.devices.thermostat import Thermostat
|
18
|
+
|
19
|
+
|
17
20
|
ATTR_THERMOSTAT_PROFILE: Final = "thermostat_profile"
|
18
21
|
ATTR_THERMOSTAT_PARAMETERS: Final = "thermostat_parameters"
|
19
22
|
ATTR_THERMOSTAT_PARAMETERS_DECODER: Final = "thermostat_parameters_decoder"
|
20
23
|
|
21
|
-
|
22
|
-
from pyplumio.devices.thermostat import Thermostat
|
24
|
+
THERMOSTAT_PARAMETER_SIZE: Final = 3
|
23
25
|
|
24
26
|
|
25
27
|
class ThermostatParameter(Parameter):
|
@@ -107,72 +109,68 @@ THERMOSTAT_PARAMETERS: tuple[ThermostatParameterDescription, ...] = (
|
|
107
109
|
)
|
108
110
|
|
109
111
|
|
110
|
-
def
|
111
|
-
|
112
|
-
) -> tuple[
|
113
|
-
"""
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
offset += 3 * description.size
|
122
|
-
|
123
|
-
return parameters, offset
|
112
|
+
def _empty_response(
|
113
|
+
offset: int, data: EventDataType | None = None
|
114
|
+
) -> tuple[EventDataType, int]:
|
115
|
+
"""Return empty response."""
|
116
|
+
return (
|
117
|
+
ensure_device_data(
|
118
|
+
data,
|
119
|
+
{ATTR_THERMOSTAT_PARAMETERS: None, ATTR_THERMOSTAT_PROFILE: None},
|
120
|
+
),
|
121
|
+
offset,
|
122
|
+
)
|
124
123
|
|
125
124
|
|
126
125
|
class ThermostatParametersStructure(StructureDecoder):
|
127
126
|
"""Represent thermostat parameters data structure."""
|
128
127
|
|
128
|
+
_offset: int
|
129
|
+
|
130
|
+
def _thermostat_parameter(
|
131
|
+
self, message: bytearray, thermostats: int, start: int, end: int
|
132
|
+
):
|
133
|
+
"""Yields thermostat parameters."""
|
134
|
+
for index in range(start, (start + end) // thermostats):
|
135
|
+
description = THERMOSTAT_PARAMETERS[index]
|
136
|
+
if (
|
137
|
+
parameter := util.unpack_parameter(
|
138
|
+
message, self._offset, size=description.size
|
139
|
+
)
|
140
|
+
) is not None:
|
141
|
+
yield (index, parameter)
|
142
|
+
|
143
|
+
self._offset += THERMOSTAT_PARAMETER_SIZE * description.size
|
144
|
+
|
129
145
|
def decode(
|
130
146
|
self, message: bytearray, offset: int = 0, data: EventDataType | None = None
|
131
147
|
) -> tuple[EventDataType, int]:
|
132
148
|
"""Decode bytes and return message data and offset."""
|
133
149
|
data = ensure_device_data(data)
|
134
|
-
|
135
|
-
if
|
136
|
-
return (
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
),
|
141
|
-
offset,
|
142
|
-
)
|
143
|
-
|
144
|
-
first_index = message[offset + 1]
|
145
|
-
last_index = message[offset + 2]
|
150
|
+
thermostats = data.get(ATTR_THERMOSTAT_COUNT, 0)
|
151
|
+
if thermostats == 0:
|
152
|
+
return _empty_response(offset, data)
|
153
|
+
|
154
|
+
start = message[offset + 1]
|
155
|
+
end = message[offset + 2]
|
146
156
|
thermostat_profile = util.unpack_parameter(message, offset + 3)
|
147
|
-
|
148
|
-
offset += 6
|
157
|
+
self._offset = offset + 6
|
149
158
|
thermostat_parameters: dict[int, list[tuple[int, ParameterDataType]]] = {}
|
150
|
-
for
|
151
|
-
parameters
|
152
|
-
message,
|
153
|
-
|
154
|
-
|
155
|
-
)
|
156
|
-
if parameters:
|
157
|
-
thermostat_parameters[index] = parameters
|
158
|
-
|
159
|
-
if not thermostat_parameters:
|
160
|
-
# No thermostat parameters detected.
|
161
|
-
return (
|
162
|
-
ensure_device_data(
|
163
|
-
data,
|
164
|
-
{ATTR_THERMOSTAT_PARAMETERS: None, ATTR_THERMOSTAT_PROFILE: None},
|
165
|
-
),
|
166
|
-
offset,
|
167
|
-
)
|
159
|
+
for thermostat in range(thermostats):
|
160
|
+
if parameters := list(
|
161
|
+
self._thermostat_parameter(message, thermostats, start, end)
|
162
|
+
):
|
163
|
+
thermostat_parameters[thermostat] = parameters
|
168
164
|
|
169
165
|
return (
|
170
166
|
ensure_device_data(
|
171
167
|
data,
|
172
168
|
{
|
173
169
|
ATTR_THERMOSTAT_PROFILE: thermostat_profile,
|
174
|
-
ATTR_THERMOSTAT_PARAMETERS:
|
170
|
+
ATTR_THERMOSTAT_PARAMETERS: None
|
171
|
+
if not thermostat_parameters
|
172
|
+
else thermostat_parameters,
|
175
173
|
},
|
176
174
|
),
|
177
|
-
|
175
|
+
self._offset,
|
178
176
|
)
|
File without changes
|
File without changes
|
File without changes
|