PyPlumIO 0.5.21__py3-none-any.whl → 0.5.22__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.21.dist-info → PyPlumIO-0.5.22.dist-info}/METADATA +7 -7
- PyPlumIO-0.5.22.dist-info/RECORD +60 -0
- {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.22.dist-info}/WHEEL +1 -1
- pyplumio/__init__.py +2 -2
- pyplumio/_version.py +2 -2
- pyplumio/connection.py +2 -10
- pyplumio/devices/__init__.py +14 -14
- pyplumio/devices/ecomax.py +117 -117
- pyplumio/devices/mixer.py +47 -41
- pyplumio/devices/thermostat.py +33 -32
- pyplumio/exceptions.py +9 -9
- pyplumio/filters.py +56 -37
- pyplumio/frames/__init__.py +6 -6
- pyplumio/frames/messages.py +4 -6
- pyplumio/helpers/data_types.py +8 -7
- pyplumio/helpers/event_manager.py +36 -25
- pyplumio/helpers/parameter.py +43 -16
- pyplumio/helpers/task_manager.py +7 -2
- pyplumio/helpers/timeout.py +0 -3
- pyplumio/helpers/uid.py +2 -2
- pyplumio/protocol.py +32 -26
- pyplumio/stream.py +2 -2
- pyplumio/structures/alerts.py +40 -31
- pyplumio/structures/ecomax_parameters.py +321 -131
- pyplumio/structures/frame_versions.py +5 -6
- pyplumio/structures/lambda_sensor.py +6 -6
- pyplumio/structures/mixer_parameters.py +74 -28
- pyplumio/structures/network_info.py +2 -3
- pyplumio/structures/product_info.py +0 -4
- pyplumio/structures/program_version.py +24 -17
- pyplumio/structures/thermostat_parameters.py +25 -12
- pyplumio/utils.py +12 -7
- PyPlumIO-0.5.21.dist-info/RECORD +0 -61
- pyplumio/helpers/typing.py +0 -29
- {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.22.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.22.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.22
|
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
|
@@ -28,18 +28,18 @@ Requires-Dist: pyplumio[docs,test] ; extra == 'dev'
|
|
28
28
|
Requires-Dist: pre-commit ==3.7.1 ; extra == 'dev'
|
29
29
|
Requires-Dist: tomli ==2.0.1 ; extra == 'dev'
|
30
30
|
Provides-Extra: docs
|
31
|
-
Requires-Dist: sphinx ==7.
|
31
|
+
Requires-Dist: sphinx ==7.4.0 ; extra == 'docs'
|
32
32
|
Requires-Dist: sphinx-rtd-theme ==2.0.0 ; extra == 'docs'
|
33
33
|
Requires-Dist: readthedocs-sphinx-search ==0.3.2 ; extra == 'docs'
|
34
34
|
Provides-Extra: test
|
35
35
|
Requires-Dist: codespell ==2.3.0 ; extra == 'test'
|
36
|
-
Requires-Dist: coverage ==7.
|
37
|
-
Requires-Dist: mypy ==1.10.
|
38
|
-
Requires-Dist: pyserial-asyncio-fast ==0.
|
36
|
+
Requires-Dist: coverage ==7.6.0 ; extra == 'test'
|
37
|
+
Requires-Dist: mypy ==1.10.1 ; extra == 'test'
|
38
|
+
Requires-Dist: pyserial-asyncio-fast ==0.13 ; extra == 'test'
|
39
39
|
Requires-Dist: pytest ==8.2.2 ; extra == 'test'
|
40
40
|
Requires-Dist: pytest-asyncio ==0.23.7 ; extra == 'test'
|
41
|
-
Requires-Dist: ruff ==0.
|
42
|
-
Requires-Dist: tox ==4.
|
41
|
+
Requires-Dist: ruff ==0.5.2 ; extra == 'test'
|
42
|
+
Requires-Dist: tox ==4.16.0 ; extra == 'test'
|
43
43
|
Requires-Dist: types-pyserial ==3.5.0.20240527 ; extra == 'test'
|
44
44
|
|
45
45
|
# PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
|
@@ -0,0 +1,60 @@
|
|
1
|
+
pyplumio/__init__.py,sha256=ditJTIOFGJDg60atHzOpiggdUrZHpSynno7MtpZUGVk,3299
|
2
|
+
pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
|
3
|
+
pyplumio/_version.py,sha256=7uyNQzq4TwYgrtNcQJPZodOcwZA-26alCK4Ab94ma2M,413
|
4
|
+
pyplumio/connection.py,sha256=QefTnJyMfFQV4f9TLRdkgP2aE9AmMjjfpFADQXgQqDE,6002
|
5
|
+
pyplumio/const.py,sha256=8rpiVbVb5R_6Rm6J2sgCnaVrkD-2Fzhd1RYMz0MBgwo,3915
|
6
|
+
pyplumio/exceptions.py,sha256=Wn-y5AJ5xfaBlHhTUVKB27_0Us8_OVHqh-sicnr9sYA,700
|
7
|
+
pyplumio/filters.py,sha256=IZkvrRAHdv6s3CplK73mHomRHpo3rnoyX2u26FVr9XU,11386
|
8
|
+
pyplumio/protocol.py,sha256=m2yPMXT2TcV-bv0jOQnwoanCpypYYh9fh7eZVOg7KTM,8108
|
9
|
+
pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
pyplumio/stream.py,sha256=IVCQFKBtRafRgUkr93p_wN5mXZAD3Jw1d091dfEIK20,4479
|
11
|
+
pyplumio/utils.py,sha256=TnBzRopinyp92wruguijxcIYmaeyNVTFX0dygI5FCMU,823
|
12
|
+
pyplumio/devices/__init__.py,sha256=nbbi65b6bYzhXdUNQjSN4ViBVoKuxcuPIPflA_C_UME,6584
|
13
|
+
pyplumio/devices/ecomax.py,sha256=Oe0_i_EvcPI12zfcKVNwL_dDeK6ipusQArzypp3LBCo,17066
|
14
|
+
pyplumio/devices/ecoster.py,sha256=J4YtPmFmFwaq4LzYf28aMmB97cRAbMsVyUdBLGki42g,313
|
15
|
+
pyplumio/devices/mixer.py,sha256=j3ysCnRpbzAycBQYiRi5y1mgHRH0EidKpdIVClWs6rA,3313
|
16
|
+
pyplumio/devices/thermostat.py,sha256=kDTtoMcMAeSDf07vWnAOLh6EQuarh7HIz2W-5Eyg2j8,2649
|
17
|
+
pyplumio/frames/__init__.py,sha256=uMjLWY0rCbCTBfXafA_TSfLORYBT0wLyhHSZEePsRxw,7504
|
18
|
+
pyplumio/frames/messages.py,sha256=7vyOjcxGDnaRlyB4jPsCt00yCc3Axme8NN7uK922DS8,3622
|
19
|
+
pyplumio/frames/requests.py,sha256=Ra8xH5oKYhkEUtadN-9ZsJKkt5xZkz5O7edQVsDhNsM,7221
|
20
|
+
pyplumio/frames/responses.py,sha256=j4awA2-MfsoPdENC4Fvae4_Oa70rDhH19ebmEoAqhh8,6532
|
21
|
+
pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
|
22
|
+
pyplumio/helpers/data_types.py,sha256=5yxHCnsoKLw5kBM3s6SxwsuKs1C0yK2khyeSrrPXQsQ,8255
|
23
|
+
pyplumio/helpers/event_manager.py,sha256=PW1cczTVHx4VBGWtBbqT7Ay6G5vSQTb_WFCkJp4HZ8o,6195
|
24
|
+
pyplumio/helpers/factory.py,sha256=eiTkYUCernUn0VNDDdEN4IyjNPrXK8vnJESXyLaqFzE,1017
|
25
|
+
pyplumio/helpers/parameter.py,sha256=Av__MjrM4k7-AwJ71t6W1zyhMq8XdTeI2vh1-p_p1-4,9437
|
26
|
+
pyplumio/helpers/schedule.py,sha256=-IZJ-CU4PhFlsE586wTw--ovDrTo2Hs4JneCHhc0e-Y,5013
|
27
|
+
pyplumio/helpers/task_manager.py,sha256=HAd69yGTRL0zQsu-ywnbLu1UXiJzgHWuhYWA--vs4lQ,1181
|
28
|
+
pyplumio/helpers/timeout.py,sha256=XM58yaz93cNsxW7Ok6hfBw8i_92HdsGFQVBhpqbCZ70,770
|
29
|
+
pyplumio/helpers/uid.py,sha256=J7gN8i8LE0g6tfL66BJbwsQQqzBBxWx7giyvqaJh4BM,976
|
30
|
+
pyplumio/structures/__init__.py,sha256=EjK-5qJZ0F7lpP2b6epvTMg9cIBl4Kn91nqNkEcLwTc,1299
|
31
|
+
pyplumio/structures/alerts.py,sha256=wX58xWr1dJgZiQtEEMLRu8bcu6dTcc-aqEIY69gYGu0,3640
|
32
|
+
pyplumio/structures/boiler_load.py,sha256=p3mOzZUU-g7A2tG_yp8podEqpI81hlsOZmHELyPNRY8,838
|
33
|
+
pyplumio/structures/boiler_power.py,sha256=72qsvccg49FdRdXv2f2K5sGpjT7wAOLFjlIGWpO-DVg,901
|
34
|
+
pyplumio/structures/ecomax_parameters.py,sha256=oHKjqeX1fCCJpGMs385-2qJ3ondCvpJRpj5rS06ibf8,28076
|
35
|
+
pyplumio/structures/fan_power.py,sha256=Q5fv-7_2NVuLeQPIVIylvgN7M8-a9D8rRUE0QGjyS3w,871
|
36
|
+
pyplumio/structures/frame_versions.py,sha256=OMWU8tjnsrRWQsMSbmCJCmiKDwBmA75BcPZ6CqvKMLc,1566
|
37
|
+
pyplumio/structures/fuel_consumption.py,sha256=_p2dI4H67Eopn7IF0Gj77A8c_8lNKhhDDAtmugxLd4s,976
|
38
|
+
pyplumio/structures/fuel_level.py,sha256=mJpp1dnRD1wXi_6EyNX7TNXosjcr905rSHOnuZ5VD74,1069
|
39
|
+
pyplumio/structures/lambda_sensor.py,sha256=JNSCiBJoM8Uk3OGbmFIigaLOntQST5U_UrmCpaQBlM0,1595
|
40
|
+
pyplumio/structures/mixer_parameters.py,sha256=l4EQSEjmpjIULUWR-ulXiYWmBLTSfRoMvK8afKTVH6M,8763
|
41
|
+
pyplumio/structures/mixer_sensors.py,sha256=O91929Ts1YXFmKdPRc1r_BYDgrqkv5QVtE1nGzLpuAI,2260
|
42
|
+
pyplumio/structures/modules.py,sha256=ukju4TQmRRJfgl94QU4zytZLU5px8nw3sgfSLn9JysU,2520
|
43
|
+
pyplumio/structures/network_info.py,sha256=rxGoTdjlUmgEzR4BjOh9XQgEqKI6OSIbhOJ8tsXocts,4063
|
44
|
+
pyplumio/structures/output_flags.py,sha256=07N0kxlvR5WZAURuChk_BqSiXR8eaQrtI5qlkgCf4Yc,1345
|
45
|
+
pyplumio/structures/outputs.py,sha256=1xsJPkjN643-aFawqVoupGatUIUJfQG_g252n051Qi0,1916
|
46
|
+
pyplumio/structures/pending_alerts.py,sha256=Uq9WpB4MW9AhDkqmDhk-g0J0h4pVq0Q50z12dYEv6kY,739
|
47
|
+
pyplumio/structures/product_info.py,sha256=uiEN6DFQlzmBvQByTirFzXQShoex0YGdFS9WI-MAxPc,2405
|
48
|
+
pyplumio/structures/program_version.py,sha256=p3Hzn1igxGyZ99jJjPswNGCAAQdJ5_-sgZPIy-MGISI,2506
|
49
|
+
pyplumio/structures/regulator_data.py,sha256=Dun3RjfHHoV2W5RTSQcAimBL0Or3O957vYQj7Pbi7CM,2309
|
50
|
+
pyplumio/structures/regulator_data_schema.py,sha256=BMshEpiP-lwTgSkbTuow9KlxCwKwQXV0nFPcBpW0SJg,1505
|
51
|
+
pyplumio/structures/schedules.py,sha256=-koo05nLkpKuj1ZPiC1NB_21MAFn1FzQ6VLC0DboYeg,6346
|
52
|
+
pyplumio/structures/statuses.py,sha256=wkoynyMRr1VREwfBC6vU48kPA8ZQ83pcXuciy2xHJrk,1166
|
53
|
+
pyplumio/structures/temperatures.py,sha256=1CDzehNmbALz1Jyt_9gZNIk52q6Wv-xQXjijVDCVYec,2337
|
54
|
+
pyplumio/structures/thermostat_parameters.py,sha256=1QkgOnDndBMWpGa8GEJLdewLkdF8UqF03yhoVzYqYJE,7796
|
55
|
+
pyplumio/structures/thermostat_sensors.py,sha256=ZmjWgYtTZ5M8Lnz_Q5N4JD8G3MvEmByPFjYsy6XZOmo,3177
|
56
|
+
PyPlumIO-0.5.22.dist-info/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
|
57
|
+
PyPlumIO-0.5.22.dist-info/METADATA,sha256=3u07pneJ8FCqq5Mj1WfLJQAd_iBqYRvlwC_I1vby3hI,5415
|
58
|
+
PyPlumIO-0.5.22.dist-info/WHEEL,sha256=rWxmBtp7hEUqVLOnTaDOPpR-cZpCDkzhhcBce-Zyd5k,91
|
59
|
+
PyPlumIO-0.5.22.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
|
60
|
+
PyPlumIO-0.5.22.dist-info/RECORD,,
|
pyplumio/__init__.py
CHANGED
@@ -10,7 +10,7 @@ from pyplumio.exceptions import (
|
|
10
10
|
ChecksumError,
|
11
11
|
ConnectionFailedError,
|
12
12
|
FrameDataError,
|
13
|
-
|
13
|
+
ProtocolError,
|
14
14
|
PyPlumIOError,
|
15
15
|
ReadError,
|
16
16
|
UnknownDeviceError,
|
@@ -97,8 +97,8 @@ __all__ = [
|
|
97
97
|
"EthernetParameters",
|
98
98
|
"Frame",
|
99
99
|
"FrameDataError",
|
100
|
-
"FrameError",
|
101
100
|
"Protocol",
|
101
|
+
"ProtocolError",
|
102
102
|
"PyPlumIOError",
|
103
103
|
"ReadError",
|
104
104
|
"SerialConnection",
|
pyplumio/_version.py
CHANGED
pyplumio/connection.py
CHANGED
@@ -131,11 +131,7 @@ class TcpConnection(Connection):
|
|
131
131
|
**kwargs: Any,
|
132
132
|
) -> None:
|
133
133
|
"""Initialize a new TCP connection."""
|
134
|
-
super().__init__(
|
135
|
-
protocol,
|
136
|
-
reconnect_on_failure,
|
137
|
-
**kwargs,
|
138
|
-
)
|
134
|
+
super().__init__(protocol, reconnect_on_failure, **kwargs)
|
139
135
|
self.host = host
|
140
136
|
self.port = port
|
141
137
|
|
@@ -171,11 +167,7 @@ class SerialConnection(Connection):
|
|
171
167
|
**kwargs: Any,
|
172
168
|
) -> None:
|
173
169
|
"""Initialize a new serial connection."""
|
174
|
-
super().__init__(
|
175
|
-
protocol,
|
176
|
-
reconnect_on_failure,
|
177
|
-
**kwargs,
|
178
|
-
)
|
170
|
+
super().__init__(protocol, reconnect_on_failure, **kwargs)
|
179
171
|
self.device = device
|
180
172
|
self.baudrate = baudrate
|
181
173
|
|
pyplumio/devices/__init__.py
CHANGED
@@ -13,8 +13,7 @@ from pyplumio.exceptions import UnknownDeviceError
|
|
13
13
|
from pyplumio.frames import DataFrameDescription, Frame, Request
|
14
14
|
from pyplumio.helpers.event_manager import EventManager
|
15
15
|
from pyplumio.helpers.factory import create_instance
|
16
|
-
from pyplumio.helpers.parameter import SET_RETRIES, Parameter
|
17
|
-
from pyplumio.helpers.typing import ParameterValueType
|
16
|
+
from pyplumio.helpers.parameter import SET_RETRIES, Parameter, ParameterValueType
|
18
17
|
from pyplumio.structures.network_info import NetworkInfo
|
19
18
|
from pyplumio.utils import to_camelcase
|
20
19
|
|
@@ -45,9 +44,9 @@ def get_device_handler(device_type: int) -> str:
|
|
45
44
|
class Device(ABC, EventManager):
|
46
45
|
"""Represents a device."""
|
47
46
|
|
48
|
-
queue: asyncio.Queue
|
47
|
+
queue: asyncio.Queue[Frame]
|
49
48
|
|
50
|
-
def __init__(self, queue: asyncio.Queue):
|
49
|
+
def __init__(self, queue: asyncio.Queue[Frame]):
|
51
50
|
"""Initialize a new device."""
|
52
51
|
super().__init__()
|
53
52
|
self.queue = queue
|
@@ -124,7 +123,7 @@ class AddressableDevice(Device, ABC):
|
|
124
123
|
_network: NetworkInfo
|
125
124
|
_setup_frames: Iterable[DataFrameDescription]
|
126
125
|
|
127
|
-
def __init__(self, queue: asyncio.Queue, network: NetworkInfo):
|
126
|
+
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
|
128
127
|
"""Initialize a new addressable device."""
|
129
128
|
super().__init__(queue)
|
130
129
|
self._network = network
|
@@ -143,19 +142,20 @@ class AddressableDevice(Device, ABC):
|
|
143
142
|
async def async_setup(self) -> bool:
|
144
143
|
"""Set up addressable device."""
|
145
144
|
results = await asyncio.gather(
|
146
|
-
*
|
145
|
+
*(
|
147
146
|
self.request(description.provides, description.frame_type)
|
148
147
|
for description in self._setup_frames
|
149
|
-
|
148
|
+
),
|
150
149
|
return_exceptions=True,
|
151
150
|
)
|
152
151
|
|
153
152
|
errors = [
|
154
|
-
result.args[1] for result in results if isinstance(result,
|
153
|
+
result.args[1] for result in results if isinstance(result, BaseException)
|
155
154
|
]
|
156
155
|
|
157
|
-
await
|
158
|
-
|
156
|
+
await asyncio.gather(
|
157
|
+
self.dispatch(ATTR_FRAME_ERRORS, errors), self.dispatch(ATTR_LOADED, True)
|
158
|
+
)
|
159
159
|
return True
|
160
160
|
|
161
161
|
async def request(
|
@@ -178,9 +178,7 @@ class AddressableDevice(Device, ABC):
|
|
178
178
|
@classmethod
|
179
179
|
async def create(cls, device_type: int, **kwargs: Any) -> AddressableDevice:
|
180
180
|
"""Create a device handler object."""
|
181
|
-
return await create_instance(
|
182
|
-
get_device_handler(device_type), cls=AddressableDevice, **kwargs
|
183
|
-
)
|
181
|
+
return await create_instance(get_device_handler(device_type), cls=cls, **kwargs)
|
184
182
|
|
185
183
|
|
186
184
|
class SubDevice(Device, ABC):
|
@@ -189,7 +187,9 @@ class SubDevice(Device, ABC):
|
|
189
187
|
parent: AddressableDevice
|
190
188
|
index: int
|
191
189
|
|
192
|
-
def __init__(
|
190
|
+
def __init__(
|
191
|
+
self, queue: asyncio.Queue[Frame], parent: AddressableDevice, index: int = 0
|
192
|
+
):
|
193
193
|
"""Initialize a new sub-device."""
|
194
194
|
super().__init__(queue)
|
195
195
|
self.parent = parent
|
pyplumio/devices/ecomax.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Generator, Iterable, Sequence
|
6
|
+
from collections.abc import Coroutine, Generator, Iterable, Sequence
|
7
7
|
import logging
|
8
8
|
import time
|
9
9
|
from typing import Any, ClassVar, Final
|
@@ -17,7 +17,7 @@ from pyplumio.const import (
|
|
17
17
|
DeviceType,
|
18
18
|
FrameType,
|
19
19
|
)
|
20
|
-
from pyplumio.devices import AddressableDevice
|
20
|
+
from pyplumio.devices import AddressableDevice
|
21
21
|
from pyplumio.devices.mixer import Mixer
|
22
22
|
from pyplumio.devices.thermostat import Thermostat
|
23
23
|
from pyplumio.filters import on_change
|
@@ -64,27 +64,38 @@ ATTR_FUEL_BURNED: Final = "fuel_burned"
|
|
64
64
|
MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * 1000000000
|
65
65
|
|
66
66
|
SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
|
67
|
-
DataFrameDescription(frame_type=FrameType.REQUEST_UID, provides=ATTR_PRODUCT),
|
68
67
|
DataFrameDescription(
|
69
|
-
frame_type=FrameType.
|
68
|
+
frame_type=FrameType.REQUEST_UID,
|
69
|
+
provides=ATTR_PRODUCT,
|
70
70
|
),
|
71
71
|
DataFrameDescription(
|
72
|
-
frame_type=FrameType.
|
72
|
+
frame_type=FrameType.REQUEST_REGULATOR_DATA_SCHEMA,
|
73
|
+
provides=ATTR_REGDATA_SCHEMA,
|
73
74
|
),
|
74
75
|
DataFrameDescription(
|
75
|
-
frame_type=FrameType.
|
76
|
+
frame_type=FrameType.REQUEST_ECOMAX_PARAMETERS,
|
77
|
+
provides=ATTR_ECOMAX_PARAMETERS,
|
76
78
|
),
|
77
79
|
DataFrameDescription(
|
78
|
-
frame_type=FrameType.
|
80
|
+
frame_type=FrameType.REQUEST_ALERTS,
|
81
|
+
provides=ATTR_TOTAL_ALERTS,
|
79
82
|
),
|
80
83
|
DataFrameDescription(
|
81
|
-
frame_type=FrameType.
|
84
|
+
frame_type=FrameType.REQUEST_SCHEDULES,
|
85
|
+
provides=ATTR_SCHEDULES,
|
86
|
+
),
|
87
|
+
DataFrameDescription(
|
88
|
+
frame_type=FrameType.REQUEST_MIXER_PARAMETERS,
|
89
|
+
provides=ATTR_MIXER_PARAMETERS,
|
82
90
|
),
|
83
91
|
DataFrameDescription(
|
84
92
|
frame_type=FrameType.REQUEST_THERMOSTAT_PARAMETERS,
|
85
93
|
provides=ATTR_THERMOSTAT_PARAMETERS,
|
86
94
|
),
|
87
|
-
DataFrameDescription(
|
95
|
+
DataFrameDescription(
|
96
|
+
frame_type=FrameType.REQUEST_PASSWORD,
|
97
|
+
provides=ATTR_PASSWORD,
|
98
|
+
),
|
88
99
|
)
|
89
100
|
|
90
101
|
_LOGGER = logging.getLogger(__name__)
|
@@ -94,11 +105,11 @@ class EcoMAX(AddressableDevice):
|
|
94
105
|
"""Represents an ecoMAX controller."""
|
95
106
|
|
96
107
|
address: ClassVar[int] = DeviceType.ECOMAX
|
97
|
-
_setup_frames:
|
108
|
+
_setup_frames: tuple[DataFrameDescription, ...] = SETUP_FRAME_TYPES
|
98
109
|
_frame_versions: dict[int, int]
|
99
110
|
_fuel_burned_timestamp_ns: int
|
100
111
|
|
101
|
-
def __init__(self, queue: asyncio.Queue, network: NetworkInfo):
|
112
|
+
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
|
102
113
|
"""Initialize a new ecoMAX controller."""
|
103
114
|
super().__init__(queue, network)
|
104
115
|
self._frame_versions = {}
|
@@ -123,11 +134,10 @@ class EcoMAX(AddressableDevice):
|
|
123
134
|
|
124
135
|
def handle_frame(self, frame: Frame) -> None:
|
125
136
|
"""Handle frame received from the ecoMAX device."""
|
126
|
-
if isinstance(frame, Request) and
|
127
|
-
|
128
|
-
FrameType.REQUEST_PROGRAM_VERSION,
|
137
|
+
if isinstance(frame, Request) and (
|
138
|
+
response := frame.response(data={ATTR_NETWORK: self._network})
|
129
139
|
):
|
130
|
-
self.queue.put_nowait(
|
140
|
+
self.queue.put_nowait(response)
|
131
141
|
|
132
142
|
super().handle_frame(frame)
|
133
143
|
|
@@ -148,12 +158,9 @@ class EcoMAX(AddressableDevice):
|
|
148
158
|
For each index, return or create an instance of the mixer class.
|
149
159
|
Once done, dispatch the 'mixers' event without waiting.
|
150
160
|
"""
|
151
|
-
mixers = self.data.setdefault(ATTR_MIXERS, {})
|
161
|
+
mixers: dict[int, Mixer] = self.data.setdefault(ATTR_MIXERS, {})
|
152
162
|
for index in indexes:
|
153
|
-
|
154
|
-
mixers[index] = Mixer(self.queue, parent=self, index=index)
|
155
|
-
|
156
|
-
yield mixers[index]
|
163
|
+
yield mixers.setdefault(index, Mixer(self.queue, parent=self, index=index))
|
157
164
|
|
158
165
|
return self.dispatch_nowait(ATTR_MIXERS, mixers)
|
159
166
|
|
@@ -164,12 +171,11 @@ class EcoMAX(AddressableDevice):
|
|
164
171
|
class. Once done, dispatch the 'thermostats' event without
|
165
172
|
waiting.
|
166
173
|
"""
|
167
|
-
thermostats = self.data.setdefault(ATTR_THERMOSTATS, {})
|
174
|
+
thermostats: dict[int, Thermostat] = self.data.setdefault(ATTR_THERMOSTATS, {})
|
168
175
|
for index in indexes:
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
yield thermostats[index]
|
176
|
+
yield thermostats.setdefault(
|
177
|
+
index, Thermostat(self.queue, parent=self, index=index)
|
178
|
+
)
|
173
179
|
|
174
180
|
return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
|
175
181
|
|
@@ -182,41 +188,42 @@ class EcoMAX(AddressableDevice):
|
|
182
188
|
and value.
|
183
189
|
"""
|
184
190
|
product: ProductInfo = await self.get(ATTR_PRODUCT)
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
191
|
+
|
192
|
+
def _ecomax_parameter_events() -> Generator[Coroutine, Any, None]:
|
193
|
+
"""Get dispatch calls for ecoMAX parameter events."""
|
194
|
+
for index, values in parameters:
|
195
|
+
try:
|
196
|
+
description = ECOMAX_PARAMETERS[product.type][index]
|
197
|
+
except IndexError:
|
198
|
+
_LOGGER.warning(
|
199
|
+
(
|
200
|
+
"Encountered unknown ecoMAX parameter (%i): %s. "
|
201
|
+
"Your device isn't fully compatible with this software and "
|
202
|
+
"may not work properly. "
|
203
|
+
"Please visit the issue tracker and open a feature "
|
204
|
+
"request to support %s"
|
205
|
+
),
|
206
|
+
index,
|
207
|
+
values,
|
208
|
+
product.model,
|
209
|
+
)
|
210
|
+
|
211
|
+
handler = (
|
212
|
+
EcomaxBinaryParameter
|
213
|
+
if isinstance(description, EcomaxBinaryParameterDescription)
|
214
|
+
else EcomaxParameter
|
215
|
+
)
|
216
|
+
yield self.dispatch(
|
217
|
+
description.name,
|
218
|
+
handler.create_or_update(
|
219
|
+
device=self,
|
220
|
+
description=description,
|
221
|
+
values=values,
|
222
|
+
index=index,
|
196
223
|
),
|
197
|
-
index,
|
198
|
-
values,
|
199
|
-
product.model,
|
200
224
|
)
|
201
|
-
return False
|
202
|
-
|
203
|
-
name = description.name
|
204
|
-
if name in self.data:
|
205
|
-
parameter: EcomaxParameter = self.data[name]
|
206
|
-
parameter.values = values
|
207
|
-
await self.dispatch(name, parameter)
|
208
|
-
continue
|
209
|
-
|
210
|
-
cls = (
|
211
|
-
EcomaxBinaryParameter
|
212
|
-
if isinstance(description, EcomaxBinaryParameterDescription)
|
213
|
-
else EcomaxParameter
|
214
|
-
)
|
215
|
-
await self.dispatch(
|
216
|
-
name,
|
217
|
-
cls(device=self, values=values, description=description, index=index),
|
218
|
-
)
|
219
225
|
|
226
|
+
await asyncio.gather(*_ecomax_parameter_events())
|
220
227
|
return True
|
221
228
|
|
222
229
|
async def _update_frame_versions(self, versions: dict[int, int]) -> None:
|
@@ -236,18 +243,15 @@ class EcoMAX(AddressableDevice):
|
|
236
243
|
"""Calculate fuel burned since last sensor's data message."""
|
237
244
|
current_timestamp_ns = time.perf_counter_ns()
|
238
245
|
time_passed_ns = current_timestamp_ns - self._fuel_burned_timestamp_ns
|
246
|
+
self._fuel_burned_timestamp_ns = current_timestamp_ns
|
239
247
|
if time_passed_ns >= MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
|
240
248
|
_LOGGER.warning(
|
241
249
|
"Skipping outdated fuel consumption data, was %i seconds old",
|
242
250
|
time_passed_ns / 1000000000,
|
243
251
|
)
|
244
252
|
else:
|
245
|
-
|
246
|
-
|
247
|
-
fuel_consumption * time_passed_ns / (3600 * 1000000000),
|
248
|
-
)
|
249
|
-
|
250
|
-
self._fuel_burned_timestamp_ns = current_timestamp_ns
|
253
|
+
fuel_burned = fuel_consumption * time_passed_ns / (3600 * 1000000000)
|
254
|
+
await self.dispatch(ATTR_FUEL_BURNED, fuel_burned)
|
251
255
|
|
252
256
|
async def _handle_mixer_parameters(
|
253
257
|
self,
|
@@ -263,15 +267,17 @@ class EcoMAX(AddressableDevice):
|
|
263
267
|
return False
|
264
268
|
|
265
269
|
await asyncio.gather(
|
266
|
-
*
|
270
|
+
*(
|
267
271
|
mixer.dispatch(ATTR_MIXER_PARAMETERS, parameters[mixer.index])
|
268
272
|
for mixer in self._mixers(indexes=parameters.keys())
|
269
|
-
|
273
|
+
)
|
270
274
|
)
|
271
275
|
|
272
276
|
return True
|
273
277
|
|
274
|
-
async def _handle_mixer_sensors(
|
278
|
+
async def _handle_mixer_sensors(
|
279
|
+
self, sensors: dict[int, dict[str, Any]] | None
|
280
|
+
) -> bool:
|
275
281
|
"""Handle mixer sensors.
|
276
282
|
|
277
283
|
For each sensor dispatch an event with the
|
@@ -282,10 +288,10 @@ class EcoMAX(AddressableDevice):
|
|
282
288
|
return False
|
283
289
|
|
284
290
|
await asyncio.gather(
|
285
|
-
*
|
291
|
+
*(
|
286
292
|
mixer.dispatch(ATTR_MIXER_SENSORS, sensors[mixer.index])
|
287
293
|
for mixer in self._mixers(indexes=sensors.keys())
|
288
|
-
|
294
|
+
)
|
289
295
|
)
|
290
296
|
|
291
297
|
return True
|
@@ -313,25 +319,27 @@ class EcoMAX(AddressableDevice):
|
|
313
319
|
self, parameters: Sequence[tuple[int, ParameterValues]]
|
314
320
|
) -> bool:
|
315
321
|
"""Add schedule parameters to the dataset."""
|
316
|
-
for index, values in parameters:
|
317
|
-
description = SCHEDULE_PARAMETERS[index]
|
318
|
-
name = description.name
|
319
|
-
if name in self.data:
|
320
|
-
parameter: ScheduleParameter = self.data[name]
|
321
|
-
parameter.values = values
|
322
|
-
await self.dispatch(name, parameter)
|
323
|
-
continue
|
324
|
-
|
325
|
-
cls = (
|
326
|
-
ScheduleBinaryParameter
|
327
|
-
if isinstance(description, ScheduleBinaryParameterDescription)
|
328
|
-
else ScheduleParameter
|
329
|
-
)
|
330
|
-
await self.dispatch(
|
331
|
-
name,
|
332
|
-
cls(device=self, values=values, description=description, index=index),
|
333
|
-
)
|
334
322
|
|
323
|
+
def _schedule_parameter_events() -> Generator[Coroutine, Any, None]:
|
324
|
+
"""Get dispatch calls for schedule parameter events."""
|
325
|
+
for index, values in parameters:
|
326
|
+
description = SCHEDULE_PARAMETERS[index]
|
327
|
+
handler = (
|
328
|
+
ScheduleBinaryParameter
|
329
|
+
if isinstance(description, ScheduleBinaryParameterDescription)
|
330
|
+
else ScheduleParameter
|
331
|
+
)
|
332
|
+
yield self.dispatch(
|
333
|
+
description.name,
|
334
|
+
handler.create_or_update(
|
335
|
+
device=self,
|
336
|
+
description=description,
|
337
|
+
values=values,
|
338
|
+
index=index,
|
339
|
+
),
|
340
|
+
)
|
341
|
+
|
342
|
+
await asyncio.gather(*_schedule_parameter_events())
|
335
343
|
return True
|
336
344
|
|
337
345
|
async def _handle_ecomax_sensors(self, sensors: dict[str, Any]) -> bool:
|
@@ -341,28 +349,20 @@ class EcoMAX(AddressableDevice):
|
|
341
349
|
value.
|
342
350
|
"""
|
343
351
|
await asyncio.gather(
|
344
|
-
*
|
352
|
+
*(self.dispatch(name, value) for name, value in sensors.items())
|
345
353
|
)
|
346
|
-
|
347
354
|
return True
|
348
355
|
|
349
356
|
async def _add_ecomax_control_parameter(self, mode: DeviceState) -> None:
|
350
357
|
"""Create ecoMAX control parameter instance and dispatch an event."""
|
351
|
-
description = ECOMAX_CONTROL_PARAMETER
|
352
|
-
name = description.name
|
353
|
-
values = ParameterValues(
|
354
|
-
value=int(mode != DeviceState.OFF), min_value=0, max_value=1
|
355
|
-
)
|
356
|
-
|
357
|
-
if name in self.data:
|
358
|
-
parameter: EcomaxBinaryParameter = self.data[name]
|
359
|
-
parameter.values = values
|
360
|
-
return await self.dispatch(name, parameter)
|
361
|
-
|
362
358
|
await self.dispatch(
|
363
|
-
name,
|
364
|
-
EcomaxBinaryParameter(
|
365
|
-
|
359
|
+
ECOMAX_CONTROL_PARAMETER.name,
|
360
|
+
EcomaxBinaryParameter.create_or_update(
|
361
|
+
description=ECOMAX_CONTROL_PARAMETER,
|
362
|
+
device=self,
|
363
|
+
values=ParameterValues(
|
364
|
+
value=int(mode != DeviceState.OFF), min_value=0, max_value=1
|
365
|
+
),
|
366
366
|
),
|
367
367
|
)
|
368
368
|
|
@@ -380,29 +380,28 @@ class EcoMAX(AddressableDevice):
|
|
380
380
|
return False
|
381
381
|
|
382
382
|
await asyncio.gather(
|
383
|
-
*
|
383
|
+
*(
|
384
384
|
thermostat.dispatch(
|
385
385
|
ATTR_THERMOSTAT_PARAMETERS, parameters[thermostat.index]
|
386
386
|
)
|
387
387
|
for thermostat in self._thermostats(indexes=parameters.keys())
|
388
|
-
|
388
|
+
)
|
389
389
|
)
|
390
|
-
|
391
390
|
return True
|
392
391
|
|
393
392
|
async def _add_thermostat_profile_parameter(
|
394
393
|
self, values: ParameterValues | None
|
395
394
|
) -> EcomaxParameter | None:
|
396
395
|
"""Add thermostat profile parameter to the dataset."""
|
397
|
-
if
|
398
|
-
return
|
399
|
-
device=self, description=THERMOSTAT_PROFILE_PARAMETER, values=values
|
400
|
-
)
|
396
|
+
if not values:
|
397
|
+
return None
|
401
398
|
|
402
|
-
return
|
399
|
+
return EcomaxParameter(
|
400
|
+
device=self, description=THERMOSTAT_PROFILE_PARAMETER, values=values
|
401
|
+
)
|
403
402
|
|
404
403
|
async def _handle_thermostat_sensors(
|
405
|
-
self, sensors: dict[int, dict[str, Any]]
|
404
|
+
self, sensors: dict[int, dict[str, Any]] | None
|
406
405
|
) -> bool:
|
407
406
|
"""Handle thermostat sensors.
|
408
407
|
|
@@ -414,10 +413,11 @@ class EcoMAX(AddressableDevice):
|
|
414
413
|
return False
|
415
414
|
|
416
415
|
await asyncio.gather(
|
417
|
-
*
|
416
|
+
*(
|
418
417
|
thermostat.dispatch(ATTR_THERMOSTAT_SENSORS, sensors[thermostat.index])
|
419
418
|
for thermostat in self._thermostats(indexes=sensors.keys())
|
420
|
-
|
419
|
+
),
|
420
|
+
return_exceptions=True,
|
421
421
|
)
|
422
422
|
|
423
423
|
return True
|
@@ -450,8 +450,8 @@ class EcoMAX(AddressableDevice):
|
|
450
450
|
|
451
451
|
async def shutdown(self) -> None:
|
452
452
|
"""Shutdown tasks for the ecoMAX controller and sub-devices."""
|
453
|
-
mixers = self.get_nowait(ATTR_MIXERS, {})
|
454
|
-
thermostats = self.get_nowait(ATTR_THERMOSTATS, {})
|
455
|
-
devices
|
456
|
-
await asyncio.gather(*
|
453
|
+
mixers: dict[str, Mixer] = self.get_nowait(ATTR_MIXERS, {})
|
454
|
+
thermostats: dict[str, Thermostat] = self.get_nowait(ATTR_THERMOSTATS, {})
|
455
|
+
devices = (mixers | thermostats).values()
|
456
|
+
await asyncio.gather(*(device.shutdown() for device in devices))
|
457
457
|
await super().shutdown()
|