PyPlumIO 0.5.20__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.20.dist-info → PyPlumIO-0.5.22.dist-info}/METADATA +8 -8
- PyPlumIO-0.5.22.dist-info/RECORD +60 -0
- {PyPlumIO-0.5.20.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 +136 -126
- pyplumio/devices/mixer.py +49 -42
- pyplumio/devices/thermostat.py +35 -33
- 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 +43 -32
- pyplumio/helpers/parameter.py +43 -16
- pyplumio/helpers/task_manager.py +9 -5
- pyplumio/helpers/timeout.py +0 -3
- pyplumio/helpers/uid.py +2 -2
- pyplumio/protocol.py +34 -33
- pyplumio/stream.py +2 -2
- pyplumio/structures/alerts.py +40 -31
- pyplumio/structures/ecomax_parameters.py +321 -133
- 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 +5 -6
- 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.20.dist-info/RECORD +0 -61
- pyplumio/helpers/typing.py +0 -29
- {PyPlumIO-0.5.20.dist-info → PyPlumIO-0.5.22.dist-info}/LICENSE +0 -0
- {PyPlumIO-0.5.20.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.
|
39
|
-
Requires-Dist: pytest ==8.2.
|
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
|
+
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,8 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
from collections.abc import Generator, Iterable, Sequence
|
7
|
-
from contextlib import suppress
|
6
|
+
from collections.abc import Coroutine, Generator, Iterable, Sequence
|
8
7
|
import logging
|
9
8
|
import time
|
10
9
|
from typing import Any, ClassVar, Final
|
@@ -65,27 +64,38 @@ ATTR_FUEL_BURNED: Final = "fuel_burned"
|
|
65
64
|
MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * 1000000000
|
66
65
|
|
67
66
|
SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
|
68
|
-
DataFrameDescription(frame_type=FrameType.REQUEST_UID, provides=ATTR_PRODUCT),
|
69
67
|
DataFrameDescription(
|
70
|
-
frame_type=FrameType.
|
68
|
+
frame_type=FrameType.REQUEST_UID,
|
69
|
+
provides=ATTR_PRODUCT,
|
71
70
|
),
|
72
71
|
DataFrameDescription(
|
73
|
-
frame_type=FrameType.
|
72
|
+
frame_type=FrameType.REQUEST_REGULATOR_DATA_SCHEMA,
|
73
|
+
provides=ATTR_REGDATA_SCHEMA,
|
74
74
|
),
|
75
75
|
DataFrameDescription(
|
76
|
-
frame_type=FrameType.
|
76
|
+
frame_type=FrameType.REQUEST_ECOMAX_PARAMETERS,
|
77
|
+
provides=ATTR_ECOMAX_PARAMETERS,
|
77
78
|
),
|
78
79
|
DataFrameDescription(
|
79
|
-
frame_type=FrameType.
|
80
|
+
frame_type=FrameType.REQUEST_ALERTS,
|
81
|
+
provides=ATTR_TOTAL_ALERTS,
|
80
82
|
),
|
81
83
|
DataFrameDescription(
|
82
|
-
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,
|
83
90
|
),
|
84
91
|
DataFrameDescription(
|
85
92
|
frame_type=FrameType.REQUEST_THERMOSTAT_PARAMETERS,
|
86
93
|
provides=ATTR_THERMOSTAT_PARAMETERS,
|
87
94
|
),
|
88
|
-
DataFrameDescription(
|
95
|
+
DataFrameDescription(
|
96
|
+
frame_type=FrameType.REQUEST_PASSWORD,
|
97
|
+
provides=ATTR_PASSWORD,
|
98
|
+
),
|
89
99
|
)
|
90
100
|
|
91
101
|
_LOGGER = logging.getLogger(__name__)
|
@@ -95,11 +105,11 @@ class EcoMAX(AddressableDevice):
|
|
95
105
|
"""Represents an ecoMAX controller."""
|
96
106
|
|
97
107
|
address: ClassVar[int] = DeviceType.ECOMAX
|
98
|
-
_setup_frames:
|
108
|
+
_setup_frames: tuple[DataFrameDescription, ...] = SETUP_FRAME_TYPES
|
99
109
|
_frame_versions: dict[int, int]
|
100
110
|
_fuel_burned_timestamp_ns: int
|
101
111
|
|
102
|
-
def __init__(self, queue: asyncio.Queue, network: NetworkInfo):
|
112
|
+
def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
|
103
113
|
"""Initialize a new ecoMAX controller."""
|
104
114
|
super().__init__(queue, network)
|
105
115
|
self._frame_versions = {}
|
@@ -124,11 +134,10 @@ class EcoMAX(AddressableDevice):
|
|
124
134
|
|
125
135
|
def handle_frame(self, frame: Frame) -> None:
|
126
136
|
"""Handle frame received from the ecoMAX device."""
|
127
|
-
if isinstance(frame, Request) and
|
128
|
-
|
129
|
-
FrameType.REQUEST_PROGRAM_VERSION,
|
137
|
+
if isinstance(frame, Request) and (
|
138
|
+
response := frame.response(data={ATTR_NETWORK: self._network})
|
130
139
|
):
|
131
|
-
self.queue.put_nowait(
|
140
|
+
self.queue.put_nowait(response)
|
132
141
|
|
133
142
|
super().handle_frame(frame)
|
134
143
|
|
@@ -149,12 +158,9 @@ class EcoMAX(AddressableDevice):
|
|
149
158
|
For each index, return or create an instance of the mixer class.
|
150
159
|
Once done, dispatch the 'mixers' event without waiting.
|
151
160
|
"""
|
152
|
-
mixers = self.data.setdefault(ATTR_MIXERS, {})
|
161
|
+
mixers: dict[int, Mixer] = self.data.setdefault(ATTR_MIXERS, {})
|
153
162
|
for index in indexes:
|
154
|
-
|
155
|
-
mixers[index] = Mixer(self.queue, parent=self, index=index)
|
156
|
-
|
157
|
-
yield mixers[index]
|
163
|
+
yield mixers.setdefault(index, Mixer(self.queue, parent=self, index=index))
|
158
164
|
|
159
165
|
return self.dispatch_nowait(ATTR_MIXERS, mixers)
|
160
166
|
|
@@ -165,12 +171,11 @@ class EcoMAX(AddressableDevice):
|
|
165
171
|
class. Once done, dispatch the 'thermostats' event without
|
166
172
|
waiting.
|
167
173
|
"""
|
168
|
-
thermostats = self.data.setdefault(ATTR_THERMOSTATS, {})
|
174
|
+
thermostats: dict[int, Thermostat] = self.data.setdefault(ATTR_THERMOSTATS, {})
|
169
175
|
for index in indexes:
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
yield thermostats[index]
|
176
|
+
yield thermostats.setdefault(
|
177
|
+
index, Thermostat(self.queue, parent=self, index=index)
|
178
|
+
)
|
174
179
|
|
175
180
|
return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
|
176
181
|
|
@@ -183,41 +188,42 @@ class EcoMAX(AddressableDevice):
|
|
183
188
|
and value.
|
184
189
|
"""
|
185
190
|
product: ProductInfo = await self.get(ATTR_PRODUCT)
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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,
|
197
223
|
),
|
198
|
-
index,
|
199
|
-
values,
|
200
|
-
product.model,
|
201
224
|
)
|
202
|
-
return False
|
203
|
-
|
204
|
-
name = description.name
|
205
|
-
if name in self.data:
|
206
|
-
parameter: EcomaxParameter = self.data[name]
|
207
|
-
parameter.values = values
|
208
|
-
await self.dispatch(name, parameter)
|
209
|
-
continue
|
210
|
-
|
211
|
-
cls = (
|
212
|
-
EcomaxBinaryParameter
|
213
|
-
if isinstance(description, EcomaxBinaryParameterDescription)
|
214
|
-
else EcomaxParameter
|
215
|
-
)
|
216
|
-
await self.dispatch(
|
217
|
-
name,
|
218
|
-
cls(device=self, values=values, description=description, index=index),
|
219
|
-
)
|
220
225
|
|
226
|
+
await asyncio.gather(*_ecomax_parameter_events())
|
221
227
|
return True
|
222
228
|
|
223
229
|
async def _update_frame_versions(self, versions: dict[int, int]) -> None:
|
@@ -237,18 +243,15 @@ class EcoMAX(AddressableDevice):
|
|
237
243
|
"""Calculate fuel burned since last sensor's data message."""
|
238
244
|
current_timestamp_ns = time.perf_counter_ns()
|
239
245
|
time_passed_ns = current_timestamp_ns - self._fuel_burned_timestamp_ns
|
246
|
+
self._fuel_burned_timestamp_ns = current_timestamp_ns
|
240
247
|
if time_passed_ns >= MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
|
241
248
|
_LOGGER.warning(
|
242
249
|
"Skipping outdated fuel consumption data, was %i seconds old",
|
243
250
|
time_passed_ns / 1000000000,
|
244
251
|
)
|
245
252
|
else:
|
246
|
-
|
247
|
-
|
248
|
-
fuel_consumption * time_passed_ns / (3600 * 1000000000),
|
249
|
-
)
|
250
|
-
|
251
|
-
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)
|
252
255
|
|
253
256
|
async def _handle_mixer_parameters(
|
254
257
|
self,
|
@@ -263,12 +266,18 @@ class EcoMAX(AddressableDevice):
|
|
263
266
|
if not parameters:
|
264
267
|
return False
|
265
268
|
|
266
|
-
|
267
|
-
|
269
|
+
await asyncio.gather(
|
270
|
+
*(
|
271
|
+
mixer.dispatch(ATTR_MIXER_PARAMETERS, parameters[mixer.index])
|
272
|
+
for mixer in self._mixers(indexes=parameters.keys())
|
273
|
+
)
|
274
|
+
)
|
268
275
|
|
269
276
|
return True
|
270
277
|
|
271
|
-
async def _handle_mixer_sensors(
|
278
|
+
async def _handle_mixer_sensors(
|
279
|
+
self, sensors: dict[int, dict[str, Any]] | None
|
280
|
+
) -> bool:
|
272
281
|
"""Handle mixer sensors.
|
273
282
|
|
274
283
|
For each sensor dispatch an event with the
|
@@ -278,14 +287,18 @@ class EcoMAX(AddressableDevice):
|
|
278
287
|
if not sensors:
|
279
288
|
return False
|
280
289
|
|
281
|
-
|
282
|
-
|
290
|
+
await asyncio.gather(
|
291
|
+
*(
|
292
|
+
mixer.dispatch(ATTR_MIXER_SENSORS, sensors[mixer.index])
|
293
|
+
for mixer in self._mixers(indexes=sensors.keys())
|
294
|
+
)
|
295
|
+
)
|
283
296
|
|
284
297
|
return True
|
285
298
|
|
286
299
|
async def _add_schedules(
|
287
300
|
self, schedules: list[tuple[int, list[list[bool]]]]
|
288
|
-
) -> dict[str,
|
301
|
+
) -> dict[str, Schedule]:
|
289
302
|
"""Add schedules to the dataset."""
|
290
303
|
return {
|
291
304
|
SCHEDULES[index]: Schedule(
|
@@ -306,25 +319,27 @@ class EcoMAX(AddressableDevice):
|
|
306
319
|
self, parameters: Sequence[tuple[int, ParameterValues]]
|
307
320
|
) -> bool:
|
308
321
|
"""Add schedule parameters to the dataset."""
|
309
|
-
for index, values in parameters:
|
310
|
-
description = SCHEDULE_PARAMETERS[index]
|
311
|
-
name = description.name
|
312
|
-
if name in self.data:
|
313
|
-
parameter: ScheduleParameter = self.data[name]
|
314
|
-
parameter.values = values
|
315
|
-
await self.dispatch(name, parameter)
|
316
|
-
continue
|
317
|
-
|
318
|
-
cls = (
|
319
|
-
ScheduleBinaryParameter
|
320
|
-
if isinstance(description, ScheduleBinaryParameterDescription)
|
321
|
-
else ScheduleParameter
|
322
|
-
)
|
323
|
-
await self.dispatch(
|
324
|
-
name,
|
325
|
-
cls(device=self, values=values, description=description, index=index),
|
326
|
-
)
|
327
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())
|
328
343
|
return True
|
329
344
|
|
330
345
|
async def _handle_ecomax_sensors(self, sensors: dict[str, Any]) -> bool:
|
@@ -333,28 +348,21 @@ class EcoMAX(AddressableDevice):
|
|
333
348
|
For each sensor dispatch an event with the sensor's name and
|
334
349
|
value.
|
335
350
|
"""
|
336
|
-
|
337
|
-
|
338
|
-
|
351
|
+
await asyncio.gather(
|
352
|
+
*(self.dispatch(name, value) for name, value in sensors.items())
|
353
|
+
)
|
339
354
|
return True
|
340
355
|
|
341
356
|
async def _add_ecomax_control_parameter(self, mode: DeviceState) -> None:
|
342
357
|
"""Create ecoMAX control parameter instance and dispatch an event."""
|
343
|
-
description = ECOMAX_CONTROL_PARAMETER
|
344
|
-
name = description.name
|
345
|
-
values = ParameterValues(
|
346
|
-
value=int(mode != DeviceState.OFF), min_value=0, max_value=1
|
347
|
-
)
|
348
|
-
|
349
|
-
if name in self.data:
|
350
|
-
parameter: EcomaxBinaryParameter = self.data[name]
|
351
|
-
parameter.values = values
|
352
|
-
return await self.dispatch(name, parameter)
|
353
|
-
|
354
358
|
await self.dispatch(
|
355
|
-
name,
|
356
|
-
EcomaxBinaryParameter(
|
357
|
-
|
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
|
+
),
|
358
366
|
),
|
359
367
|
)
|
360
368
|
|
@@ -371,26 +379,29 @@ class EcoMAX(AddressableDevice):
|
|
371
379
|
if not parameters:
|
372
380
|
return False
|
373
381
|
|
374
|
-
|
375
|
-
|
376
|
-
|
382
|
+
await asyncio.gather(
|
383
|
+
*(
|
384
|
+
thermostat.dispatch(
|
385
|
+
ATTR_THERMOSTAT_PARAMETERS, parameters[thermostat.index]
|
386
|
+
)
|
387
|
+
for thermostat in self._thermostats(indexes=parameters.keys())
|
377
388
|
)
|
378
|
-
|
389
|
+
)
|
379
390
|
return True
|
380
391
|
|
381
392
|
async def _add_thermostat_profile_parameter(
|
382
393
|
self, values: ParameterValues | None
|
383
394
|
) -> EcomaxParameter | None:
|
384
395
|
"""Add thermostat profile parameter to the dataset."""
|
385
|
-
if
|
386
|
-
return
|
387
|
-
device=self, description=THERMOSTAT_PROFILE_PARAMETER, values=values
|
388
|
-
)
|
396
|
+
if not values:
|
397
|
+
return None
|
389
398
|
|
390
|
-
return
|
399
|
+
return EcomaxParameter(
|
400
|
+
device=self, description=THERMOSTAT_PROFILE_PARAMETER, values=values
|
401
|
+
)
|
391
402
|
|
392
403
|
async def _handle_thermostat_sensors(
|
393
|
-
self, sensors: dict[int, dict[str, Any]]
|
404
|
+
self, sensors: dict[int, dict[str, Any]] | None
|
394
405
|
) -> bool:
|
395
406
|
"""Handle thermostat sensors.
|
396
407
|
|
@@ -401,10 +412,13 @@ class EcoMAX(AddressableDevice):
|
|
401
412
|
if not sensors:
|
402
413
|
return False
|
403
414
|
|
404
|
-
|
405
|
-
|
406
|
-
ATTR_THERMOSTAT_SENSORS, sensors[thermostat.index]
|
407
|
-
|
415
|
+
await asyncio.gather(
|
416
|
+
*(
|
417
|
+
thermostat.dispatch(ATTR_THERMOSTAT_SENSORS, sensors[thermostat.index])
|
418
|
+
for thermostat in self._thermostats(indexes=sensors.keys())
|
419
|
+
),
|
420
|
+
return_exceptions=True,
|
421
|
+
)
|
408
422
|
|
409
423
|
return True
|
410
424
|
|
@@ -436,12 +450,8 @@ class EcoMAX(AddressableDevice):
|
|
436
450
|
|
437
451
|
async def shutdown(self) -> None:
|
438
452
|
"""Shutdown tasks for the ecoMAX controller and sub-devices."""
|
439
|
-
mixers = self.get_nowait(ATTR_MIXERS, {})
|
440
|
-
thermostats = self.get_nowait(ATTR_THERMOSTATS, {})
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
with suppress(AttributeError):
|
445
|
-
await self.regdata.shutdown()
|
446
|
-
|
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))
|
447
457
|
await super().shutdown()
|