PyPlumIO 0.5.20__tar.gz → 0.5.21__tar.gz

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.
Files changed (142) hide show
  1. {pyplumio-0.5.20 → pyplumio-0.5.21}/PKG-INFO +5 -5
  2. {pyplumio-0.5.20 → pyplumio-0.5.21}/PyPlumIO.egg-info/PKG-INFO +5 -5
  3. {pyplumio-0.5.20 → pyplumio-0.5.21}/PyPlumIO.egg-info/requires.txt +4 -4
  4. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/_version.py +2 -2
  5. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/devices/ecomax.py +33 -23
  6. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/devices/mixer.py +3 -2
  7. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/devices/thermostat.py +3 -2
  8. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/event_manager.py +9 -9
  9. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/task_manager.py +3 -4
  10. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/protocol.py +6 -11
  11. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/ecomax_parameters.py +0 -2
  12. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/network_info.py +3 -3
  13. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyproject.toml +4 -4
  14. {pyplumio-0.5.20 → pyplumio-0.5.21}/requirements_test.txt +4 -4
  15. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/frames/test_messages.py +1 -1
  16. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/frames/test_responses.py +1 -1
  17. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/helpers/test_event_manager.py +25 -14
  18. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/test_protocol.py +11 -5
  19. {pyplumio-0.5.20 → pyplumio-0.5.21}/.gitattributes +0 -0
  20. {pyplumio-0.5.20 → pyplumio-0.5.21}/.github/CODE_OF_CONDUCT.md +0 -0
  21. {pyplumio-0.5.20 → pyplumio-0.5.21}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  22. {pyplumio-0.5.20 → pyplumio-0.5.21}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  23. {pyplumio-0.5.20 → pyplumio-0.5.21}/.github/dependabot.yml +0 -0
  24. {pyplumio-0.5.20 → pyplumio-0.5.21}/.github/workflows/ci.yml +0 -0
  25. {pyplumio-0.5.20 → pyplumio-0.5.21}/.github/workflows/codeql-analysis.yml +0 -0
  26. {pyplumio-0.5.20 → pyplumio-0.5.21}/.github/workflows/deploy.yml +0 -0
  27. {pyplumio-0.5.20 → pyplumio-0.5.21}/.github/workflows/documentation.yml +0 -0
  28. {pyplumio-0.5.20 → pyplumio-0.5.21}/.gitignore +0 -0
  29. {pyplumio-0.5.20 → pyplumio-0.5.21}/.pre-commit-config.yaml +0 -0
  30. {pyplumio-0.5.20 → pyplumio-0.5.21}/.vscode/settings.json +0 -0
  31. {pyplumio-0.5.20 → pyplumio-0.5.21}/LICENSE +0 -0
  32. {pyplumio-0.5.20 → pyplumio-0.5.21}/MANIFEST.in +0 -0
  33. {pyplumio-0.5.20 → pyplumio-0.5.21}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  34. {pyplumio-0.5.20 → pyplumio-0.5.21}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  35. {pyplumio-0.5.20 → pyplumio-0.5.21}/PyPlumIO.egg-info/top_level.txt +0 -0
  36. {pyplumio-0.5.20 → pyplumio-0.5.21}/README.md +0 -0
  37. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/Makefile +0 -0
  38. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/make.bat +0 -0
  39. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/source/callbacks.rst +0 -0
  40. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/source/conf.py +0 -0
  41. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/source/connecting.rst +0 -0
  42. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/source/index.rst +0 -0
  43. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/source/mixers_thermostats.rst +0 -0
  44. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/source/protocol.rst +0 -0
  45. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/source/reading.rst +0 -0
  46. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/source/schedules.rst +0 -0
  47. {pyplumio-0.5.20 → pyplumio-0.5.21}/docs/source/writing.rst +0 -0
  48. {pyplumio-0.5.20 → pyplumio-0.5.21}/images/ecomax.png +0 -0
  49. {pyplumio-0.5.20 → pyplumio-0.5.21}/images/rs485.png +0 -0
  50. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/__init__.py +0 -0
  51. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/__main__.py +0 -0
  52. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/connection.py +0 -0
  53. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/const.py +0 -0
  54. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/devices/__init__.py +0 -0
  55. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/devices/ecoster.py +0 -0
  56. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/exceptions.py +0 -0
  57. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/filters.py +0 -0
  58. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/frames/__init__.py +0 -0
  59. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/frames/messages.py +0 -0
  60. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/frames/requests.py +0 -0
  61. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/frames/responses.py +0 -0
  62. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/__init__.py +0 -0
  63. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/data_types.py +0 -0
  64. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/factory.py +0 -0
  65. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/parameter.py +0 -0
  66. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/schedule.py +0 -0
  67. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/timeout.py +0 -0
  68. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/typing.py +0 -0
  69. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/helpers/uid.py +0 -0
  70. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/py.typed +0 -0
  71. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/stream.py +0 -0
  72. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/__init__.py +0 -0
  73. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/alerts.py +0 -0
  74. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/boiler_load.py +0 -0
  75. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/boiler_power.py +0 -0
  76. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/fan_power.py +0 -0
  77. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/frame_versions.py +0 -0
  78. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/fuel_consumption.py +0 -0
  79. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/fuel_level.py +0 -0
  80. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/lambda_sensor.py +0 -0
  81. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/mixer_parameters.py +0 -0
  82. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/mixer_sensors.py +0 -0
  83. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/modules.py +0 -0
  84. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/output_flags.py +0 -0
  85. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/outputs.py +0 -0
  86. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/pending_alerts.py +0 -0
  87. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/product_info.py +0 -0
  88. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/program_version.py +0 -0
  89. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/regulator_data.py +0 -0
  90. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/regulator_data_schema.py +0 -0
  91. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/schedules.py +0 -0
  92. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/statuses.py +0 -0
  93. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/temperatures.py +0 -0
  94. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/thermostat_parameters.py +0 -0
  95. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/structures/thermostat_sensors.py +0 -0
  96. {pyplumio-0.5.20 → pyplumio-0.5.21}/pyplumio/utils.py +0 -0
  97. {pyplumio-0.5.20 → pyplumio-0.5.21}/requirements.txt +0 -0
  98. {pyplumio-0.5.20 → pyplumio-0.5.21}/requirements_docs.txt +0 -0
  99. {pyplumio-0.5.20 → pyplumio-0.5.21}/setup.cfg +0 -0
  100. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/__init__.py +0 -0
  101. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/conftest.py +0 -0
  102. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/frames/test_init.py +0 -0
  103. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/frames/test_requests.py +0 -0
  104. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/helpers/__init__.py +0 -0
  105. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/helpers/test_data_types.py +0 -0
  106. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/helpers/test_factory.py +0 -0
  107. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/helpers/test_parameter.py +0 -0
  108. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/helpers/test_schedule.py +0 -0
  109. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/helpers/test_task_manager.py +0 -0
  110. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/helpers/test_timeout.py +0 -0
  111. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/helpers/test_uid.py +0 -0
  112. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/ruff.toml +0 -0
  113. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/test_connection.py +0 -0
  114. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/test_devices.py +0 -0
  115. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/test_filters.py +0 -0
  116. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/test_init.py +0 -0
  117. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/test_main.py +0 -0
  118. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/test_stream.py +0 -0
  119. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/test_utils.py +0 -0
  120. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/messages/regulator_data.json +0 -0
  121. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/messages/sensor_data.json +0 -0
  122. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/requests/alerts.json +0 -0
  123. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/requests/ecomax_control.json +0 -0
  124. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/requests/ecomax_parameters.json +0 -0
  125. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/requests/mixer_parameters.json +0 -0
  126. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  127. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  128. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/requests/set_schedule.json +0 -0
  129. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  130. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/requests/thermostat_parameters.json +0 -0
  131. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/alerts.json +0 -0
  132. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/device_available.json +0 -0
  133. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/ecomax_parameters.json +0 -0
  134. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/mixer_parameters.json +0 -0
  135. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/password.json +0 -0
  136. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/program_version.json +0 -0
  137. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/regulator_data_schema.json +0 -0
  138. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/schedules.json +0 -0
  139. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/thermostat_parameters.json +0 -0
  140. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/responses/uid.json +0 -0
  141. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  142. {pyplumio-0.5.20 → pyplumio-0.5.21}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyPlumIO
3
- Version: 0.5.20
3
+ Version: 0.5.21
4
4
  Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
5
5
  Author-email: Denis Paavilainen <denpa@denpa.pro>
6
6
  License: MIT License
@@ -27,11 +27,11 @@ Provides-Extra: test
27
27
  Requires-Dist: codespell==2.3.0; extra == "test"
28
28
  Requires-Dist: coverage==7.5.3; extra == "test"
29
29
  Requires-Dist: mypy==1.10.0; extra == "test"
30
- Requires-Dist: pyserial-asyncio-fast==0.11; extra == "test"
31
- Requires-Dist: pytest==8.2.1; extra == "test"
30
+ Requires-Dist: pyserial-asyncio-fast==0.12; extra == "test"
31
+ Requires-Dist: pytest==8.2.2; extra == "test"
32
32
  Requires-Dist: pytest-asyncio==0.23.7; extra == "test"
33
- Requires-Dist: ruff==0.4.7; extra == "test"
34
- Requires-Dist: tox==4.15.0; extra == "test"
33
+ Requires-Dist: ruff==0.4.9; extra == "test"
34
+ Requires-Dist: tox==4.15.1; extra == "test"
35
35
  Requires-Dist: types-pyserial==3.5.0.20240527; extra == "test"
36
36
  Provides-Extra: docs
37
37
  Requires-Dist: sphinx==7.3.7; extra == "docs"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyPlumIO
3
- Version: 0.5.20
3
+ Version: 0.5.21
4
4
  Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
5
5
  Author-email: Denis Paavilainen <denpa@denpa.pro>
6
6
  License: MIT License
@@ -27,11 +27,11 @@ Provides-Extra: test
27
27
  Requires-Dist: codespell==2.3.0; extra == "test"
28
28
  Requires-Dist: coverage==7.5.3; extra == "test"
29
29
  Requires-Dist: mypy==1.10.0; extra == "test"
30
- Requires-Dist: pyserial-asyncio-fast==0.11; extra == "test"
31
- Requires-Dist: pytest==8.2.1; extra == "test"
30
+ Requires-Dist: pyserial-asyncio-fast==0.12; extra == "test"
31
+ Requires-Dist: pytest==8.2.2; extra == "test"
32
32
  Requires-Dist: pytest-asyncio==0.23.7; extra == "test"
33
- Requires-Dist: ruff==0.4.7; extra == "test"
34
- Requires-Dist: tox==4.15.0; extra == "test"
33
+ Requires-Dist: ruff==0.4.9; extra == "test"
34
+ Requires-Dist: tox==4.15.1; extra == "test"
35
35
  Requires-Dist: types-pyserial==3.5.0.20240527; extra == "test"
36
36
  Provides-Extra: docs
37
37
  Requires-Dist: sphinx==7.3.7; extra == "docs"
@@ -14,9 +14,9 @@ readthedocs-sphinx-search==0.3.2
14
14
  codespell==2.3.0
15
15
  coverage==7.5.3
16
16
  mypy==1.10.0
17
- pyserial-asyncio-fast==0.11
18
- pytest==8.2.1
17
+ pyserial-asyncio-fast==0.12
18
+ pytest==8.2.2
19
19
  pytest-asyncio==0.23.7
20
- ruff==0.4.7
21
- tox==4.15.0
20
+ ruff==0.4.9
21
+ tox==4.15.1
22
22
  types-pyserial==3.5.0.20240527
@@ -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.20'
16
- __version_tuple__ = version_tuple = (0, 5, 20)
15
+ __version__ = version = '0.5.21'
16
+ __version_tuple__ = version_tuple = (0, 5, 21)
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  from collections.abc import Generator, Iterable, Sequence
7
- from contextlib import suppress
8
7
  import logging
9
8
  import time
10
9
  from typing import Any, ClassVar, Final
@@ -18,7 +17,7 @@ from pyplumio.const import (
18
17
  DeviceType,
19
18
  FrameType,
20
19
  )
21
- from pyplumio.devices import AddressableDevice
20
+ from pyplumio.devices import AddressableDevice, SubDevice
22
21
  from pyplumio.devices.mixer import Mixer
23
22
  from pyplumio.devices.thermostat import Thermostat
24
23
  from pyplumio.filters import on_change
@@ -263,8 +262,12 @@ class EcoMAX(AddressableDevice):
263
262
  if not parameters:
264
263
  return False
265
264
 
266
- for mixer in self._mixers(parameters.keys()):
267
- await mixer.dispatch(ATTR_MIXER_PARAMETERS, parameters[mixer.index])
265
+ await asyncio.gather(
266
+ *[
267
+ mixer.dispatch(ATTR_MIXER_PARAMETERS, parameters[mixer.index])
268
+ for mixer in self._mixers(indexes=parameters.keys())
269
+ ]
270
+ )
268
271
 
269
272
  return True
270
273
 
@@ -278,14 +281,18 @@ class EcoMAX(AddressableDevice):
278
281
  if not sensors:
279
282
  return False
280
283
 
281
- for mixer in self._mixers(sensors.keys()):
282
- await mixer.dispatch(ATTR_MIXER_SENSORS, sensors[mixer.index])
284
+ await asyncio.gather(
285
+ *[
286
+ mixer.dispatch(ATTR_MIXER_SENSORS, sensors[mixer.index])
287
+ for mixer in self._mixers(indexes=sensors.keys())
288
+ ]
289
+ )
283
290
 
284
291
  return True
285
292
 
286
293
  async def _add_schedules(
287
294
  self, schedules: list[tuple[int, list[list[bool]]]]
288
- ) -> dict[str, Any]:
295
+ ) -> dict[str, Schedule]:
289
296
  """Add schedules to the dataset."""
290
297
  return {
291
298
  SCHEDULES[index]: Schedule(
@@ -333,8 +340,9 @@ class EcoMAX(AddressableDevice):
333
340
  For each sensor dispatch an event with the sensor's name and
334
341
  value.
335
342
  """
336
- for name, value in sensors.items():
337
- await self.dispatch(name, value)
343
+ await asyncio.gather(
344
+ *[self.dispatch(name, value) for name, value in sensors.items()]
345
+ )
338
346
 
339
347
  return True
340
348
 
@@ -371,10 +379,14 @@ class EcoMAX(AddressableDevice):
371
379
  if not parameters:
372
380
  return False
373
381
 
374
- for thermostat in self._thermostats(parameters.keys()):
375
- await thermostat.dispatch(
376
- ATTR_THERMOSTAT_PARAMETERS, parameters[thermostat.index]
377
- )
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())
388
+ ]
389
+ )
378
390
 
379
391
  return True
380
392
 
@@ -401,10 +413,12 @@ class EcoMAX(AddressableDevice):
401
413
  if not sensors:
402
414
  return False
403
415
 
404
- for thermostat in self._thermostats(sensors.keys()):
405
- await thermostat.dispatch(
406
- ATTR_THERMOSTAT_SENSORS, sensors[thermostat.index]
407
- )
416
+ await asyncio.gather(
417
+ *[
418
+ thermostat.dispatch(ATTR_THERMOSTAT_SENSORS, sensors[thermostat.index])
419
+ for thermostat in self._thermostats(indexes=sensors.keys())
420
+ ]
421
+ )
408
422
 
409
423
  return True
410
424
 
@@ -438,10 +452,6 @@ class EcoMAX(AddressableDevice):
438
452
  """Shutdown tasks for the ecoMAX controller and sub-devices."""
439
453
  mixers = self.get_nowait(ATTR_MIXERS, {})
440
454
  thermostats = self.get_nowait(ATTR_THERMOSTATS, {})
441
- for subdevice in (mixers | thermostats).values():
442
- await subdevice.shutdown()
443
-
444
- with suppress(AttributeError):
445
- await self.regdata.shutdown()
446
-
455
+ devices: Iterable[SubDevice] = (mixers | thermostats).values()
456
+ await asyncio.gather(*[device.shutdown() for device in devices])
447
457
  await super().shutdown()
@@ -37,8 +37,9 @@ class Mixer(SubDevice):
37
37
  For each sensor dispatch an event with the
38
38
  sensor's name and value.
39
39
  """
40
- for name, value in sensors.items():
41
- await self.dispatch(name, value)
40
+ await asyncio.gather(
41
+ *[self.dispatch(name, value) for name, value in sensors.items()]
42
+ )
42
43
 
43
44
  return True
44
45
 
@@ -33,8 +33,9 @@ class Thermostat(SubDevice):
33
33
  For each sensor dispatch an event with the
34
34
  sensor's name and value.
35
35
  """
36
- for name, value in sensors.items():
37
- await self.dispatch(name, value)
36
+ await asyncio.gather(
37
+ *[self.dispatch(name, value) for name, value in sensors.items()]
38
+ )
38
39
 
39
40
  return True
40
41
 
@@ -138,16 +138,16 @@ class EventManager(TaskManager):
138
138
  """Call a registered callbacks and dispatch the event without waiting."""
139
139
  self.create_task(self.dispatch(name, value))
140
140
 
141
- def load(self, data: dict[str, Any]) -> None:
142
- """Load an event data."""
143
-
144
- async def _dispatch_events(data: dict[str, Any]) -> None:
145
- """Dispatch events for a loaded data."""
146
- for key, value in data.items():
147
- await self.dispatch(key, value)
148
-
141
+ async def load(self, data: dict[str, Any]) -> None:
142
+ """Load event data."""
149
143
  self.data = data
150
- self.create_task(_dispatch_events(data))
144
+ await asyncio.gather(
145
+ *[self.dispatch(name, value) for name, value in data.items()]
146
+ )
147
+
148
+ def load_nowait(self, data: dict[str, Any]) -> None:
149
+ """Load event data without waiting."""
150
+ self.create_task(self.load(data))
151
151
 
152
152
  def create_event(self, name: str) -> asyncio.Event:
153
153
  """Create an event."""
@@ -18,15 +18,14 @@ class TaskManager:
18
18
 
19
19
  def create_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task:
20
20
  """Create asyncio task and store a reference for it."""
21
- task: asyncio.Task = asyncio.create_task(coro)
21
+ task = asyncio.create_task(coro)
22
22
  self._tasks.add(task)
23
23
  task.add_done_callback(self._tasks.discard)
24
24
  return task
25
25
 
26
- def cancel_tasks(self) -> None:
26
+ def cancel_tasks(self) -> bool:
27
27
  """Cancel all tasks."""
28
- for task in self._tasks:
29
- task.cancel()
28
+ return all(task.cancel() for task in self._tasks)
30
29
 
31
30
  async def wait_until_done(self, return_exceptions: bool = True) -> None:
32
31
  """Wait for all tasks to complete."""
@@ -107,8 +107,7 @@ class Queues:
107
107
 
108
108
  async def join(self) -> None:
109
109
  """Wait for queues to finish."""
110
- for queue in (self.read, self.write):
111
- await queue.join()
110
+ await asyncio.gather(self.read.join(), self.write.join())
112
111
 
113
112
 
114
113
  class AsyncProtocol(Protocol, EventManager):
@@ -171,22 +170,18 @@ class AsyncProtocol(Protocol, EventManager):
171
170
  return
172
171
 
173
172
  self.connected.clear()
174
- for device in self.data.values():
175
- # Notify devices about connection loss.
176
- await device.dispatch(ATTR_CONNECTED, False)
177
-
178
173
  await self.close_writer()
179
- for callback in self.on_connection_lost:
180
- await callback()
174
+ await asyncio.gather(
175
+ *[device.dispatch(ATTR_CONNECTED, False) for device in self.data.values()]
176
+ )
177
+ await asyncio.gather(*[callback() for callback in self.on_connection_lost])
181
178
 
182
179
  async def shutdown(self) -> None:
183
180
  """Shutdown protocol tasks."""
184
181
  await self._queues.join()
185
182
  self.cancel_tasks()
186
183
  await self.wait_until_done()
187
- for device in self.data.values():
188
- await device.shutdown()
189
-
184
+ await asyncio.gather(*[device.shutdown() for device in self.data.values()])
190
185
  if self.connected.is_set():
191
186
  self.connected.clear()
192
187
  await self.close_writer()
@@ -116,8 +116,6 @@ class EcomaxBinaryParameterDescription(
116
116
  ):
117
117
  """Represents an ecoMAX binary parameter description."""
118
118
 
119
- __slots__ = ()
120
-
121
119
 
122
120
  ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
123
121
  ProductType.ECOMAX_P: (
@@ -18,7 +18,7 @@ DEFAULT_NETMASK: Final = "255.255.255.0"
18
18
  NETWORK_INFO_SIZE: Final = 25
19
19
 
20
20
 
21
- @dataclass
21
+ @dataclass(frozen=True)
22
22
  class EthernetParameters:
23
23
  """Represents an ethernet parameters."""
24
24
 
@@ -35,7 +35,7 @@ class EthernetParameters:
35
35
  status: bool = True
36
36
 
37
37
 
38
- @dataclass
38
+ @dataclass(frozen=True)
39
39
  class WirelessParameters(EthernetParameters):
40
40
  """Represents a wireless network parameters."""
41
41
 
@@ -50,7 +50,7 @@ class WirelessParameters(EthernetParameters):
50
50
  signal_quality: int = 100
51
51
 
52
52
 
53
- @dataclass
53
+ @dataclass(frozen=True)
54
54
  class NetworkInfo:
55
55
  """Represents a network parameters."""
56
56
 
@@ -35,11 +35,11 @@ test = [
35
35
  "codespell==2.3.0",
36
36
  "coverage==7.5.3",
37
37
  "mypy==1.10.0",
38
- "pyserial-asyncio-fast==0.11",
39
- "pytest==8.2.1",
38
+ "pyserial-asyncio-fast==0.12",
39
+ "pytest==8.2.2",
40
40
  "pytest-asyncio==0.23.7",
41
- "ruff==0.4.7",
42
- "tox==4.15.0",
41
+ "ruff==0.4.9",
42
+ "tox==4.15.1",
43
43
  "types-pyserial==3.5.0.20240527"
44
44
  ]
45
45
  docs = [
@@ -2,10 +2,10 @@ codespell==2.3.0
2
2
  coverage==7.5.3
3
3
  mypy==1.10.0
4
4
  pre-commit==3.7.1
5
- pyserial-asyncio-fast==0.11
6
- pytest==8.2.1
5
+ pyserial-asyncio-fast==0.12
6
+ pytest==8.2.2
7
7
  pytest-asyncio==0.23.7
8
- ruff==0.4.7
8
+ ruff==0.4.9
9
9
  tomli==2.0.1
10
- tox==4.15.0
10
+ tox==4.15.1
11
11
  types-pyserial==3.5.0.20240527
@@ -37,7 +37,7 @@ async def test_regulator_data_message(ecomax: EcoMAX, schema, regdata) -> None:
37
37
  """Test a regulator data message."""
38
38
  frame = RegulatorDataMessage(message=regdata["message"])
39
39
  frame.sender_device = ecomax
40
- frame.sender_device.load(schema["data"])
40
+ frame.sender_device.load_nowait(schema["data"])
41
41
  await frame.sender_device.wait_until_done()
42
42
 
43
43
  if regdata["id"] == "unknown_regulator_data_version":
@@ -124,7 +124,7 @@ async def test_thermostat_parameters_response(ecomax: EcoMAX, message, data) ->
124
124
  """Test a thermostat parameters response."""
125
125
  frame = ThermostatParametersResponse(message=message)
126
126
  frame.sender_device = ecomax
127
- frame.sender_device.load({ATTR_THERMOSTATS_AVAILABLE: 3})
127
+ frame.sender_device.load_nowait({ATTR_THERMOSTATS_AVAILABLE: 3})
128
128
  await frame.sender_device.wait_until_done()
129
129
 
130
130
  assert frame.data == data
@@ -38,16 +38,39 @@ async def test_wait_for(event_manager: EventManager) -> None:
38
38
 
39
39
 
40
40
  async def test_get(event_manager: EventManager) -> None:
41
- """Test getting an event value asynchronously."""
41
+ """Test getting an event value."""
42
42
  assert await event_manager.get("test_key") == "test_value"
43
43
 
44
44
 
45
45
  def test_get_nowait(event_manager: EventManager) -> None:
46
- """Test getting an event value."""
46
+ """Test getting an event value without waiting."""
47
47
  assert event_manager.get_nowait("test_key") == "test_value"
48
48
  assert event_manager.get_nowait("test_key2") is None
49
49
 
50
50
 
51
+ async def test_load(event_manager: EventManager) -> None:
52
+ """Test loading event data."""
53
+ callback = AsyncMock(return_value=True)
54
+ callback2 = AsyncMock(return_value=True)
55
+ event_manager.subscribe("test_key1", callback)
56
+ event_manager.subscribe("test_key2", callback2)
57
+ event_manager.load_nowait({"test_key2": "test_value2"})
58
+ await event_manager.wait_until_done()
59
+ callback.assert_not_awaited()
60
+ callback2.assert_awaited_once_with("test_value2")
61
+
62
+
63
+ async def test_load_nowait(event_manager: EventManager) -> None:
64
+ """Test loading event data without waiting."""
65
+ callback = AsyncMock(return_value=True)
66
+ callback2 = AsyncMock(return_value=True)
67
+ event_manager.subscribe("test_key1", callback)
68
+ event_manager.subscribe("test_key2", callback2)
69
+ await event_manager.load({"test_key2": "test_value2"})
70
+ callback.assert_not_awaited()
71
+ callback2.assert_awaited_once_with("test_value2")
72
+
73
+
51
74
  async def test_subscribe(event_manager: EventManager) -> None:
52
75
  """Test subscribing to an event."""
53
76
  callback = AsyncMock(return_value=True)
@@ -78,18 +101,6 @@ async def test_unsubscribe(event_manager: EventManager) -> None:
78
101
  callback.assert_not_awaited()
79
102
 
80
103
 
81
- async def test_load(event_manager: EventManager) -> None:
82
- """Test loading an event data."""
83
- callback = AsyncMock(return_value=True)
84
- callback2 = AsyncMock(return_value=True)
85
- event_manager.subscribe("test_key1", callback)
86
- event_manager.subscribe("test_key2", callback2)
87
- event_manager.load({"test_key2": "test_value2"})
88
- await event_manager.wait_until_done()
89
- callback.assert_not_awaited()
90
- callback2.assert_awaited_once_with("test_value2")
91
-
92
-
93
104
  async def test_create_event(event_manager: EventManager) -> None:
94
105
  """Test creating an event."""
95
106
  event = event_manager.create_event("test")
@@ -195,7 +195,7 @@ async def test_async_protocol_connection_lost() -> None:
195
195
  @patch("asyncio.wait")
196
196
  @patch("asyncio.gather", new_callable=AsyncMock)
197
197
  @patch("pyplumio.protocol.AsyncProtocol.cancel_tasks")
198
- @patch("pyplumio.devices.ecomax.EcoMAX.shutdown")
198
+ @patch("pyplumio.devices.ecomax.EcoMAX.shutdown", new_callable=Mock)
199
199
  async def test_async_protocol_shutdown(
200
200
  mock_shutdown,
201
201
  mock_cancel_tasks,
@@ -205,8 +205,8 @@ async def test_async_protocol_shutdown(
205
205
  bypass_asyncio_events,
206
206
  ) -> None:
207
207
  """Test shutting down connection with an async protocol."""
208
- mock_read_queue = AsyncMock()
209
- mock_write_queue = AsyncMock()
208
+ mock_read_queue = Mock()
209
+ mock_write_queue = Mock()
210
210
 
211
211
  mock_writer = AsyncMock()
212
212
  mock_writer.close = AsyncMock()
@@ -231,9 +231,15 @@ async def test_async_protocol_shutdown(
231
231
  ):
232
232
  await async_protocol.shutdown()
233
233
 
234
- mock_shutdown.assert_awaited_once()
235
234
  mock_cancel_tasks.assert_called_once()
236
- mock_gather.assert_awaited_once_with(*async_protocol.tasks, return_exceptions=True)
235
+ mock_gather.await_count = 3
236
+ mock_gather.assert_has_awaits(
237
+ [
238
+ call(mock_read_queue.join(), mock_write_queue.join()),
239
+ call(*async_protocol.tasks, return_exceptions=True),
240
+ call(mock_shutdown()),
241
+ ]
242
+ )
237
243
  mock_writer.close.assert_awaited_once()
238
244
  assert async_protocol.writer is None
239
245
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes