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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyPlumIO
3
- Version: 0.5.21
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.3.7 ; extra == 'docs'
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.5.3 ; extra == 'test'
37
- Requires-Dist: mypy ==1.10.0 ; extra == 'test'
38
- Requires-Dist: pyserial-asyncio-fast ==0.12 ; extra == 'test'
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.4.9 ; extra == 'test'
42
- Requires-Dist: tox ==4.15.1 ; extra == 'test'
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (71.0.4)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
pyplumio/__init__.py CHANGED
@@ -10,7 +10,7 @@ from pyplumio.exceptions import (
10
10
  ChecksumError,
11
11
  ConnectionFailedError,
12
12
  FrameDataError,
13
- FrameError,
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
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.5.21'
16
- __version_tuple__ = version_tuple = (0, 5, 21)
15
+ __version__ = version = '0.5.22'
16
+ __version_tuple__ = version_tuple = (0, 5, 22)
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
 
@@ -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, ValueError)
153
+ result.args[1] for result in results if isinstance(result, BaseException)
155
154
  ]
156
155
 
157
- await self.dispatch(ATTR_FRAME_ERRORS, errors)
158
- await self.dispatch(ATTR_LOADED, True)
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__(self, queue: asyncio.Queue, parent: AddressableDevice, index: int = 0):
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
@@ -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, SubDevice
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.REQUEST_REGULATOR_DATA_SCHEMA, provides=ATTR_REGDATA_SCHEMA
68
+ frame_type=FrameType.REQUEST_UID,
69
+ provides=ATTR_PRODUCT,
70
70
  ),
71
71
  DataFrameDescription(
72
- frame_type=FrameType.REQUEST_ECOMAX_PARAMETERS, provides=ATTR_ECOMAX_PARAMETERS
72
+ frame_type=FrameType.REQUEST_REGULATOR_DATA_SCHEMA,
73
+ provides=ATTR_REGDATA_SCHEMA,
73
74
  ),
74
75
  DataFrameDescription(
75
- frame_type=FrameType.REQUEST_ALERTS, provides=ATTR_TOTAL_ALERTS
76
+ frame_type=FrameType.REQUEST_ECOMAX_PARAMETERS,
77
+ provides=ATTR_ECOMAX_PARAMETERS,
76
78
  ),
77
79
  DataFrameDescription(
78
- frame_type=FrameType.REQUEST_SCHEDULES, provides=ATTR_SCHEDULES
80
+ frame_type=FrameType.REQUEST_ALERTS,
81
+ provides=ATTR_TOTAL_ALERTS,
79
82
  ),
80
83
  DataFrameDescription(
81
- frame_type=FrameType.REQUEST_MIXER_PARAMETERS, provides=ATTR_MIXER_PARAMETERS
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(frame_type=FrameType.REQUEST_PASSWORD, provides=ATTR_PASSWORD),
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: Iterable[DataFrameDescription] = SETUP_FRAME_TYPES
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 frame.frame_type in (
127
- FrameType.REQUEST_CHECK_DEVICE,
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(frame.response(data={ATTR_NETWORK: self._network}))
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
- if index not in mixers:
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
- if index not in thermostats:
170
- thermostats[index] = Thermostat(self.queue, parent=self, index=index)
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
- for index, values in parameters:
186
- try:
187
- description = ECOMAX_PARAMETERS[product.type][index]
188
- except IndexError:
189
- _LOGGER.warning(
190
- (
191
- "Encountered unknown ecoMAX parameter (%i): %s. "
192
- "Your device isn't fully compatible with this software and "
193
- "may not work properly. "
194
- "Please visit the issue tracker and open a feature "
195
- "request to support %s"
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
- await self.dispatch(
246
- ATTR_FUEL_BURNED,
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(self, sensors: dict[int, dict[str, Any]]) -> bool:
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
- *[self.dispatch(name, value) for name, value in sensors.items()]
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
- device=self, description=ECOMAX_CONTROL_PARAMETER, values=values
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 values is not None:
398
- return EcomaxParameter(
399
- device=self, description=THERMOSTAT_PROFILE_PARAMETER, values=values
400
- )
396
+ if not values:
397
+ return None
401
398
 
402
- return None
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: Iterable[SubDevice] = (mixers | thermostats).values()
456
- await asyncio.gather(*[device.shutdown() for device in devices])
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()