PyPlumIO 0.6.3__tar.gz → 0.6.4__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 (153) hide show
  1. {pyplumio-0.6.3 → pyplumio-0.6.4}/.pre-commit-config.yaml +1 -1
  2. {pyplumio-0.6.3 → pyplumio-0.6.4}/PKG-INFO +3 -3
  3. {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/PKG-INFO +3 -3
  4. {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/requires.txt +2 -2
  5. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/_version.py +3 -3
  6. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/connection.py +7 -7
  7. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/__init__.py +20 -14
  8. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/ecomax.py +16 -12
  9. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/__init__.py +2 -2
  10. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/protocol.py +68 -99
  11. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/stream.py +6 -5
  12. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/network_info.py +23 -21
  13. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/schedules.py +1 -1
  14. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/utils.py +6 -4
  15. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyproject.toml +2 -2
  16. {pyplumio-0.6.3 → pyplumio-0.6.4}/requirements_test.txt +2 -2
  17. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/conftest.py +1 -1
  18. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_ecomax.py +6 -6
  19. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_ecoster.py +1 -1
  20. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_init.py +3 -1
  21. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/test_init.py +9 -9
  22. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/test_mixers.py +3 -1
  23. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/test_thermostats.py +3 -1
  24. pyplumio-0.6.4/tests/test_protocol.py +257 -0
  25. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_stream.py +2 -2
  26. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/device_available.json +4 -4
  27. pyplumio-0.6.3/tests/test_protocol.py +0 -469
  28. {pyplumio-0.6.3 → pyplumio-0.6.4}/.gitattributes +0 -0
  29. {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/CODE_OF_CONDUCT.md +0 -0
  30. {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  31. {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  32. {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/dependabot.yml +0 -0
  33. {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/workflows/ci.yml +0 -0
  34. {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/workflows/codeql.yml +0 -0
  35. {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/workflows/deploy.yml +0 -0
  36. {pyplumio-0.6.3 → pyplumio-0.6.4}/.github/workflows/documentation.yml +0 -0
  37. {pyplumio-0.6.3 → pyplumio-0.6.4}/.gitignore +0 -0
  38. {pyplumio-0.6.3 → pyplumio-0.6.4}/.qlty/qlty.toml +0 -0
  39. {pyplumio-0.6.3 → pyplumio-0.6.4}/.vscode/settings.json +0 -0
  40. {pyplumio-0.6.3 → pyplumio-0.6.4}/LICENSE +0 -0
  41. {pyplumio-0.6.3 → pyplumio-0.6.4}/MANIFEST.in +0 -0
  42. {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  43. {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  44. {pyplumio-0.6.3 → pyplumio-0.6.4}/PyPlumIO.egg-info/top_level.txt +0 -0
  45. {pyplumio-0.6.3 → pyplumio-0.6.4}/README.md +0 -0
  46. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/Makefile +0 -0
  47. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/make.bat +0 -0
  48. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/callbacks.rst +0 -0
  49. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/conf.py +0 -0
  50. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/connecting.rst +0 -0
  51. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/frames.rst +0 -0
  52. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/index.rst +0 -0
  53. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/mixers_thermostats.rst +0 -0
  54. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/protocol.rst +0 -0
  55. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/reading.rst +0 -0
  56. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/schedules.rst +0 -0
  57. {pyplumio-0.6.3 → pyplumio-0.6.4}/docs/source/writing.rst +0 -0
  58. {pyplumio-0.6.3 → pyplumio-0.6.4}/images/ecomax.png +0 -0
  59. {pyplumio-0.6.3 → pyplumio-0.6.4}/images/rs485.png +0 -0
  60. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/__init__.py +0 -0
  61. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/__main__.py +0 -0
  62. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/const.py +0 -0
  63. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/data_types.py +0 -0
  64. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/ecoster.py +0 -0
  65. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/mixer.py +0 -0
  66. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/devices/thermostat.py +0 -0
  67. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/exceptions.py +0 -0
  68. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/filters.py +0 -0
  69. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/frames/__init__.py +0 -0
  70. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/frames/messages.py +0 -0
  71. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/frames/requests.py +0 -0
  72. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/frames/responses.py +0 -0
  73. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/__init__.py +0 -0
  74. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/async_cache.py +0 -0
  75. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/event_manager.py +0 -0
  76. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/factory.py +0 -0
  77. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/helpers/task_manager.py +0 -0
  78. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/custom/__init__.py +0 -0
  79. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/custom/ecomax_860d3_hb.py +0 -0
  80. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/ecomax.py +0 -0
  81. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/mixer.py +0 -0
  82. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/parameters/thermostat.py +0 -0
  83. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/py.typed +0 -0
  84. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/__init__.py +0 -0
  85. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/alerts.py +0 -0
  86. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/ecomax_parameters.py +0 -0
  87. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/frame_versions.py +0 -0
  88. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/mixer_parameters.py +0 -0
  89. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/product_info.py +0 -0
  90. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/program_version.py +0 -0
  91. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/regulator_data.py +0 -0
  92. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/regulator_data_schema.py +0 -0
  93. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/sensor_data.py +0 -0
  94. {pyplumio-0.6.3 → pyplumio-0.6.4}/pyplumio/structures/thermostat_parameters.py +0 -0
  95. {pyplumio-0.6.3 → pyplumio-0.6.4}/requirements.txt +0 -0
  96. {pyplumio-0.6.3 → pyplumio-0.6.4}/requirements_docs.txt +0 -0
  97. {pyplumio-0.6.3 → pyplumio-0.6.4}/setup.cfg +0 -0
  98. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/__init__.py +0 -0
  99. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/__init__.py +0 -0
  100. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_mixer.py +0 -0
  101. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/devices/test_thermostat.py +0 -0
  102. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/frames/test_init.py +0 -0
  103. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/frames/test_messages.py +0 -0
  104. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/frames/test_requests.py +0 -0
  105. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/frames/test_responses.py +0 -0
  106. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/__init__.py +0 -0
  107. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_async_cache.py +0 -0
  108. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_event_manager.py +0 -0
  109. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_factory.py +0 -0
  110. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_task_manager.py +0 -0
  111. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/helpers/test_uid.py +0 -0
  112. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/__init__.py +0 -0
  113. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/custom/__init__.py +0 -0
  114. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/custom/test_ecomax_860d3_hb.py +0 -0
  115. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/custom/test_init.py +0 -0
  116. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/parameters/test_ecomax.py +0 -0
  117. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/ruff.toml +0 -0
  118. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/__init__.py +0 -0
  119. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_alerts.py +0 -0
  120. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_ecomax_parameters.py +0 -0
  121. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_frame_versions.py +0 -0
  122. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_mixer_parameters.py +0 -0
  123. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_product_info.py +0 -0
  124. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/structures/test_schedules.py +0 -0
  125. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_connection.py +0 -0
  126. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_data_types.py +0 -0
  127. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_filters.py +0 -0
  128. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_init.py +0 -0
  129. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_main.py +0 -0
  130. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/test_utils.py +0 -0
  131. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/messages/regulator_data.json +0 -0
  132. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/messages/sensor_data.json +0 -0
  133. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/parameters/ecomax_860d3_hb.json +0 -0
  134. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/alerts.json +0 -0
  135. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/ecomax_control.json +0 -0
  136. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/ecomax_parameters.json +0 -0
  137. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/mixer_parameters.json +0 -0
  138. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  139. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  140. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/set_schedule.json +0 -0
  141. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  142. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/requests/thermostat_parameters.json +0 -0
  143. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/alerts.json +0 -0
  144. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/ecomax_parameters.json +0 -0
  145. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/mixer_parameters.json +0 -0
  146. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/password.json +0 -0
  147. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/program_version.json +0 -0
  148. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/regulator_data_schema.json +0 -0
  149. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/schedules.json +0 -0
  150. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/thermostat_parameters.json +0 -0
  151. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/responses/uid.json +0 -0
  152. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  153. {pyplumio-0.6.3 → pyplumio-0.6.4}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -3,7 +3,7 @@
3
3
  # See https://pre-commit.com/hooks.html for more hooks
4
4
  repos:
5
5
  - repo: https://github.com/astral-sh/ruff-pre-commit
6
- rev: v0.14.0
6
+ rev: v0.14.4
7
7
  hooks:
8
8
  - id: ruff
9
9
  args:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.6.3
3
+ Version: 0.6.4
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
@@ -32,8 +32,8 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
32
32
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
33
33
  Requires-Dist: pytest==8.4.2; extra == "test"
34
34
  Requires-Dist: pytest-asyncio==1.2.0; extra == "test"
35
- Requires-Dist: ruff==0.14.1; extra == "test"
36
- Requires-Dist: tox==4.31.0; extra == "test"
35
+ Requires-Dist: ruff==0.14.3; extra == "test"
36
+ Requires-Dist: tox==4.32.0; extra == "test"
37
37
  Requires-Dist: types-pyserial==3.5.0.20251001; extra == "test"
38
38
  Provides-Extra: docs
39
39
  Requires-Dist: sphinx==8.1.3; extra == "docs"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.6.3
3
+ Version: 0.6.4
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
@@ -32,8 +32,8 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
32
32
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
33
33
  Requires-Dist: pytest==8.4.2; extra == "test"
34
34
  Requires-Dist: pytest-asyncio==1.2.0; extra == "test"
35
- Requires-Dist: ruff==0.14.1; extra == "test"
36
- Requires-Dist: tox==4.31.0; extra == "test"
35
+ Requires-Dist: ruff==0.14.3; extra == "test"
36
+ Requires-Dist: tox==4.32.0; extra == "test"
37
37
  Requires-Dist: types-pyserial==3.5.0.20251001; extra == "test"
38
38
  Provides-Extra: docs
39
39
  Requires-Dist: sphinx==8.1.3; extra == "docs"
@@ -19,6 +19,6 @@ numpy<3.0.0,>=2.0.0
19
19
  pyserial-asyncio-fast==0.16
20
20
  pytest==8.4.2
21
21
  pytest-asyncio==1.2.0
22
- ruff==0.14.1
23
- tox==4.31.0
22
+ ruff==0.14.3
23
+ tox==4.32.0
24
24
  types-pyserial==3.5.0.20251001
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.6.3'
32
- __version_tuple__ = version_tuple = (0, 6, 3)
31
+ __version__ = version = '0.6.4'
32
+ __version_tuple__ = version_tuple = (0, 6, 4)
33
33
 
34
- __commit_id__ = commit_id = 'g1c504461d'
34
+ __commit_id__ = commit_id = 'gc17c7ed5e'
@@ -19,8 +19,8 @@ from pyplumio.utils import timeout
19
19
 
20
20
  _LOGGER = logging.getLogger(__name__)
21
21
 
22
- CONNECT_TIMEOUT: Final = 5
23
- RECONNECT_TIMEOUT: Final = 20
22
+ TRY_CONNECT_FOR_SECONDS: Final = 5
23
+ RECONNECT_AFTER_SECONDS: Final = 20
24
24
 
25
25
  try:
26
26
  import serial_asyncio_fast as pyserial_asyncio
@@ -88,9 +88,9 @@ class Connection(ABC, TaskManager):
88
88
  except ConnectionFailedError:
89
89
  _LOGGER.error(
90
90
  "Can't connect to the device, retrying in %.1f seconds",
91
- RECONNECT_TIMEOUT,
91
+ RECONNECT_AFTER_SECONDS,
92
92
  )
93
- await asyncio.sleep(RECONNECT_TIMEOUT)
93
+ await asyncio.sleep(RECONNECT_AFTER_SECONDS)
94
94
  self.create_task(self._reconnect(), name="reconnect_task")
95
95
 
96
96
  async def connect(self) -> None:
@@ -126,7 +126,7 @@ class Connection(ABC, TaskManager):
126
126
  """Return connection options."""
127
127
  return self._options
128
128
 
129
- @timeout(CONNECT_TIMEOUT)
129
+ @timeout(TRY_CONNECT_FOR_SECONDS)
130
130
  @abstractmethod
131
131
  async def _open_connection(
132
132
  self,
@@ -162,7 +162,7 @@ class TcpConnection(Connection):
162
162
  f"TcpConnection(host={self.host}, port={self.port}, options={self.options})"
163
163
  )
164
164
 
165
- @timeout(CONNECT_TIMEOUT)
165
+ @timeout(TRY_CONNECT_FOR_SECONDS)
166
166
  async def _open_connection(
167
167
  self,
168
168
  ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
@@ -203,7 +203,7 @@ class SerialConnection(Connection):
203
203
  f"options={self.options})"
204
204
  )
205
205
 
206
- @timeout(CONNECT_TIMEOUT)
206
+ @timeout(TRY_CONNECT_FOR_SECONDS)
207
207
  async def _open_connection(
208
208
  self,
209
209
  ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
@@ -52,14 +52,18 @@ def get_device_handler(device_type: int) -> str:
52
52
  class Device(ABC, EventManager):
53
53
  """Represents a device."""
54
54
 
55
- __slots__ = ("queue",)
55
+ __slots__ = ("_write_queue",)
56
56
 
57
- queue: asyncio.Queue[Frame]
57
+ _write_queue: asyncio.Queue[Frame]
58
58
 
59
- def __init__(self, queue: asyncio.Queue[Frame]) -> None:
59
+ def __init__(self, write_queue: asyncio.Queue[Frame]) -> None:
60
60
  """Initialize a new device."""
61
61
  super().__init__()
62
- self.queue = queue
62
+ self._write_queue = write_queue
63
+
64
+ def queue_send(self, frame: Frame) -> None:
65
+ """Send frame to the write queue."""
66
+ self._write_queue.put_nowait(frame)
63
67
 
64
68
  async def set(
65
69
  self,
@@ -130,17 +134,19 @@ class PhysicalDevice(Device, ABC):
130
134
  virtual devices associated with them via parent property.
131
135
  """
132
136
 
133
- __slots__ = ("address", "_network", "_frame_versions")
137
+ __slots__ = ("address", "_network_info", "_frame_versions")
134
138
 
135
139
  address: ClassVar[int]
136
140
 
137
- _network: NetworkInfo
141
+ _network_info: NetworkInfo
138
142
  _frame_versions: dict[int, int]
139
143
 
140
- def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
144
+ def __init__(
145
+ self, write_queue: asyncio.Queue[Frame], network_info: NetworkInfo
146
+ ) -> None:
141
147
  """Initialize a new physical device."""
142
- super().__init__(queue)
143
- self._network = network
148
+ super().__init__(write_queue)
149
+ self._network_info = network_info
144
150
  self._frame_versions = {}
145
151
 
146
152
  @event_listener(filter=on_change)
@@ -167,7 +173,7 @@ class PhysicalDevice(Device, ABC):
167
173
  """Request frame version from the device."""
168
174
  _LOGGER.debug("Updating frame %s to version %i", repr(frame_type), version)
169
175
  request = await Request.create(frame_type, recipient=self.address)
170
- self.queue.put_nowait(request)
176
+ self.queue_send(request)
171
177
 
172
178
  def has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
173
179
  """Return True if frame data is up to date, False otherwise."""
@@ -199,7 +205,7 @@ class PhysicalDevice(Device, ABC):
199
205
  initial_retries = retries
200
206
  while retries > 0:
201
207
  try:
202
- self.queue.put_nowait(request)
208
+ self.queue_send(request)
203
209
  return await self.get(name, timeout=timeout)
204
210
  except asyncio.TimeoutError:
205
211
  retries -= 1
@@ -211,7 +217,7 @@ class PhysicalDevice(Device, ABC):
211
217
  )
212
218
 
213
219
  @classmethod
214
- async def create(cls, device_type: int, **kwargs: Any) -> PhysicalDevice:
220
+ async def create(cls, device_type: DeviceType, **kwargs: Any) -> PhysicalDevice:
215
221
  """Create a physical device handler object."""
216
222
  return await create_instance(get_device_handler(device_type), cls=cls, **kwargs)
217
223
 
@@ -225,10 +231,10 @@ class VirtualDevice(Device, ABC):
225
231
  index: int
226
232
 
227
233
  def __init__(
228
- self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
234
+ self, write_queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
229
235
  ) -> None:
230
236
  """Initialize a new sub-device."""
231
- super().__init__(queue)
237
+ super().__init__(write_queue)
232
238
  self.parent = parent
233
239
  self.index = index
234
240
 
@@ -42,7 +42,7 @@ from pyplumio.structures.ecomax_parameters import (
42
42
  ATTR_ECOMAX_PARAMETERS,
43
43
  )
44
44
  from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
45
- from pyplumio.structures.network_info import ATTR_NETWORK, NetworkInfo
45
+ from pyplumio.structures.network_info import ATTR_NETWORK_INFO, NetworkInfo
46
46
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
47
47
  from pyplumio.structures.regulator_data_schema import ATTR_REGDATA_SCHEMA
48
48
  from pyplumio.structures.schedules import (
@@ -121,7 +121,7 @@ REQUIRED_KEYS: tuple[DataKey, ...] = (
121
121
 
122
122
  REQUIRED_TYPES = [frame_type for _, frame_type in REQUIRED_KEYS]
123
123
 
124
- SETUP_TIMEOUT: Final = 60.0
124
+ WAIT_FOR_SETUP_SECONDS: Final = 60.0
125
125
 
126
126
 
127
127
  @device_handler(DeviceType.ECOMAX)
@@ -132,34 +132,38 @@ class EcoMAX(PhysicalDevice):
132
132
 
133
133
  _fuel_meter: FuelMeter
134
134
 
135
- def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
135
+ def __init__(
136
+ self, write_queue: asyncio.Queue[Frame], network_info: NetworkInfo
137
+ ) -> None:
136
138
  """Initialize a new ecoMAX controller."""
137
- super().__init__(queue, network)
139
+ super().__init__(write_queue, network_info)
138
140
  self._fuel_meter = FuelMeter()
139
141
 
140
142
  def handle_frame(self, frame: Frame) -> None:
141
143
  """Handle frame received from the ecoMAX device."""
142
144
  if isinstance(frame, Request) and (
143
- response := frame.response(data={ATTR_NETWORK: self._network})
145
+ response := frame.response(data={ATTR_NETWORK_INFO: self._network_info})
144
146
  ):
145
- self.queue.put_nowait(response)
147
+ self.queue_send(response)
146
148
 
147
149
  super().handle_frame(frame)
148
150
 
149
151
  def _mixers(self, indexes: Iterable[int]) -> Generator[Mixer]:
150
- """Iterate through the mixer indexes.
152
+ """Iterate through indexes and yield a Mixer instance.
151
153
 
152
154
  For each index, return or create an instance of the mixer class.
153
155
  Once done, dispatch the 'mixers' event without waiting.
154
156
  """
155
157
  mixers: dict[int, Mixer] = self._data.setdefault(ATTR_MIXERS, {})
156
158
  for index in indexes:
157
- yield mixers.setdefault(index, Mixer(self.queue, parent=self, index=index))
159
+ yield mixers.setdefault(
160
+ index, Mixer(self._write_queue, parent=self, index=index)
161
+ )
158
162
 
159
163
  return self.dispatch_nowait(ATTR_MIXERS, mixers)
160
164
 
161
165
  def _thermostats(self, indexes: Iterable[int]) -> Generator[Thermostat]:
162
- """Iterate through the thermostat indexes.
166
+ """Iterate through indexes and yield a Thermostat instance.
163
167
 
164
168
  For each index, return or create an instance of the thermostat
165
169
  class. Once done, dispatch the 'thermostats' event without
@@ -168,7 +172,7 @@ class EcoMAX(PhysicalDevice):
168
172
  thermostats: dict[int, Thermostat] = self._data.setdefault(ATTR_THERMOSTATS, {})
169
173
  for index in indexes:
170
174
  yield thermostats.setdefault(
171
- index, Thermostat(self.queue, parent=self, index=index)
175
+ index, Thermostat(self._write_queue, parent=self, index=index)
172
176
  )
173
177
 
174
178
  return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
@@ -220,11 +224,11 @@ class EcoMAX(PhysicalDevice):
220
224
  _LOGGER.debug("Setting up device entry")
221
225
 
222
226
  try:
223
- await self.wait_for(ATTR_SENSORS, timeout=SETUP_TIMEOUT)
227
+ await self.wait_for(ATTR_SENSORS, timeout=WAIT_FOR_SETUP_SECONDS)
224
228
  except asyncio.TimeoutError:
225
229
  _LOGGER.error(
226
230
  "Could not setup device entry; no response from device for %u seconds",
227
- SETUP_TIMEOUT,
231
+ WAIT_FOR_SETUP_SECONDS,
228
232
  )
229
233
  return False
230
234
 
@@ -204,7 +204,7 @@ class Parameter(ABC):
204
204
  self._values = replace(self._values, value=value)
205
205
  request = await self.create_request()
206
206
  if self.description.optimistic:
207
- await self.device.queue.put(request)
207
+ self.device.queue_send(request)
208
208
  return True
209
209
 
210
210
  self.update_done.clear()
@@ -233,7 +233,7 @@ class Parameter(ABC):
233
233
 
234
234
  async def _send_update_request(self, request: Request, timeout: float) -> bool:
235
235
  """Send update request to the remote and confirm the result."""
236
- await self.device.queue.put(request)
236
+ self.device.queue_send(request)
237
237
  with suppress(asyncio.TimeoutError):
238
238
  # Wait for the update to be done
239
239
  await asyncio.wait_for(self.update_done.wait(), timeout=timeout)
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
  import asyncio
7
7
  from collections.abc import Awaitable, Callable
8
+ from contextlib import suppress
8
9
  from dataclasses import dataclass, field
9
10
  from datetime import datetime
10
11
  import logging
@@ -46,7 +47,7 @@ class Protocol(ABC):
46
47
  self.writer = None
47
48
  self._on_connection_lost = set()
48
49
 
49
- async def close_writer(self) -> None:
50
+ async def _close_writer(self) -> None:
50
51
  """Ensure that writer is closed."""
51
52
  if self.writer:
52
53
  await self.writer.close()
@@ -91,26 +92,14 @@ class DummyProtocol(Protocol):
91
92
  """Close writer and call connection lost callbacks."""
92
93
  if self.connected.is_set():
93
94
  self.connected.clear()
94
- await self.close_writer()
95
+ await self._close_writer()
95
96
  await asyncio.gather(*(callback() for callback in self.on_connection_lost))
96
97
 
97
98
  async def shutdown(self) -> None:
98
99
  """Shutdown the protocol."""
99
100
  if self.connected.is_set():
100
101
  self.connected.clear()
101
- await self.close_writer()
102
-
103
-
104
- @dataclass(slots=True)
105
- class Queues:
106
- """Represents asyncio queues."""
107
-
108
- read: asyncio.Queue[Frame] = field(default_factory=asyncio.Queue)
109
- write: asyncio.Queue[Frame] = field(default_factory=asyncio.Queue)
110
-
111
- async def join(self) -> None:
112
- """Wait for queues to finish."""
113
- await asyncio.gather(self.read.join(), self.write.join())
102
+ await self._close_writer()
114
103
 
115
104
 
116
105
  NEVER: Final = "never"
@@ -194,7 +183,7 @@ class DeviceStatistics:
194
183
  """Return a hash of the statistics based on unique address."""
195
184
  return self.address
196
185
 
197
- async def update_last_seen(self, _: Any) -> None:
186
+ async def update_last_seen(self, _: Any | None = None) -> None:
198
187
  """Update last seen property."""
199
188
  self.last_seen = datetime.now()
200
189
 
@@ -202,41 +191,34 @@ class DeviceStatistics:
202
191
  class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
203
192
  """Represents an async protocol.
204
193
 
205
- This protocol implements producer-consumers pattern using
206
- asyncio queues.
207
-
208
- The frame producer tries to read frames from the write queue.
209
- If any is available, it sends them to the device via frame writer.
194
+ This protocol implements frame handling using a single task and
195
+ write queue.
210
196
 
211
- It then reads stream via frame reader and puts received frame
212
- into the read queue.
197
+ The frame handler processes frames in both directions:
198
+ - Sends frames from the write queue to the device via frame writer
199
+ - Reads incoming frames via frame reader and processes them
213
200
 
214
- Frame consumers read frames from the read queue, create device
215
- entry, if needed, and send frame to the entry for the processing.
201
+ Each received frame is passed to appropriate device handler for
202
+ further processing.
216
203
  """
217
204
 
218
- consumers_count: int
219
- _network: NetworkInfo
220
- _queues: Queues
221
- _entry_lock: asyncio.Lock
205
+ _network_info: NetworkInfo
206
+ _write_queue: asyncio.Queue[Frame]
222
207
  _statistics: Statistics
223
208
 
224
209
  def __init__(
225
210
  self,
226
211
  ethernet_parameters: EthernetParameters | None = None,
227
212
  wireless_parameters: WirelessParameters | None = None,
228
- consumers_count: int = 3,
229
213
  ) -> None:
230
214
  """Initialize a new async protocol."""
231
215
  super().__init__()
232
- self.consumers_count = consumers_count
233
- self._network = NetworkInfo(
234
- eth=ethernet_parameters or EthernetParameters(status=False),
235
- wlan=wireless_parameters or WirelessParameters(status=False),
216
+ self._network_info = NetworkInfo(
217
+ ethernet=ethernet_parameters or EthernetParameters(status=False),
218
+ wireless=wireless_parameters or WirelessParameters(status=False),
236
219
  )
237
- self._entry_lock = asyncio.Lock()
220
+ self._write_queue = asyncio.Queue()
238
221
  self._statistics = Statistics()
239
- self._queues = Queues()
240
222
 
241
223
  def connection_established(
242
224
  self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
@@ -244,17 +226,11 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
244
226
  """Start frame producer and consumers."""
245
227
  self.reader = FrameReader(reader)
246
228
  self.writer = FrameWriter(writer)
247
- self._queues.write.put_nowait(StartMasterRequest(recipient=DeviceType.ECOMAX))
229
+ self._write_queue.put_nowait(StartMasterRequest(recipient=DeviceType.ECOMAX))
248
230
  self.create_task(
249
- self.frame_producer(self._queues, reader=self.reader, writer=self.writer),
250
- name="frame_producer_task",
231
+ self.frame_handler(reader=self.reader, writer=self.writer),
232
+ name="frame_handler_task",
251
233
  )
252
- for consumer_id in range(self.consumers_count):
253
- self.create_task(
254
- self.frame_consumer(self._queues.read),
255
- name=f"frame_consumer_task ({consumer_id})",
256
- )
257
-
258
234
  for device in self.data.values():
259
235
  device.dispatch_nowait(ATTR_CONNECTED, True)
260
236
 
@@ -262,57 +238,57 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
262
238
  self.statistics.reset_transfer_statistics()
263
239
  self.statistics.connected_since = datetime.now()
264
240
 
265
- async def _connection_close(self) -> None:
266
- """Close the connection if it is established."""
241
+ async def _mark_disconnected(self) -> None:
242
+ """Mark as protocol as disconnected."""
267
243
  self.connected.clear()
268
- await asyncio.gather(
269
- *(device.dispatch(ATTR_CONNECTED, False) for device in self.data.values())
270
- )
271
- await self.close_writer()
244
+ for device in self.data.values():
245
+ device.dispatch_nowait(ATTR_CONNECTED, False)
246
+
247
+ def _clear_write_queue(self) -> None:
248
+ """Clear the write queue."""
249
+ with suppress(asyncio.QueueEmpty):
250
+ while not self._write_queue.empty():
251
+ self._write_queue.get_nowait()
252
+ self._write_queue.task_done()
272
253
 
273
254
  async def connection_lost(self) -> None:
274
255
  """Close the connection and call connection lost callbacks."""
275
256
  if self.connected.is_set():
276
- await self._connection_close()
257
+ await self._mark_disconnected()
277
258
  await asyncio.gather(*(callback() for callback in self.on_connection_lost))
259
+ await self._close_writer()
278
260
 
279
261
  async def shutdown(self) -> None:
280
262
  """Shutdown the protocol and close the connection."""
281
- await self._queues.join()
282
- self.cancel_tasks()
283
- await self.wait_until_done()
284
263
  if self.connected.is_set():
285
- await self._connection_close()
264
+ await self._mark_disconnected()
286
265
  await asyncio.gather(*(device.shutdown() for device in self.data.values()))
266
+ await self._close_writer()
287
267
 
288
- async def _write_from_queue(
289
- self, writer: FrameWriter, queue: asyncio.Queue[Frame]
290
- ) -> None:
291
- """Send frame from the queue to the remote device."""
292
- frame = await queue.get()
293
- await writer.write(frame)
294
- queue.task_done()
295
- self.statistics.update_sent(frame)
296
-
297
- async def _read_into_queue(
298
- self, reader: FrameReader, queue: asyncio.Queue[Frame]
299
- ) -> None:
300
- """Read frame from the remote device into the queue."""
301
- if frame := await reader.read():
302
- queue.put_nowait(frame)
303
- self.statistics.update_received(frame)
268
+ self._clear_write_queue()
269
+ self.cancel_tasks()
270
+ await self.wait_until_done()
304
271
 
305
- async def frame_producer(
306
- self, queues: Queues, reader: FrameReader, writer: FrameWriter
307
- ) -> None:
272
+ async def frame_handler(self, reader: FrameReader, writer: FrameWriter) -> None:
308
273
  """Handle frame reads and writes."""
309
274
  await self.connected.wait()
310
275
  while self.connected.is_set():
311
276
  try:
312
- if not queues.write.empty():
313
- await self._write_from_queue(writer, queues.write)
277
+ frame: Frame | None
278
+
279
+ # Handle pending writes.
280
+ if not self._write_queue.empty():
281
+ frame = await self._write_queue.get()
282
+ await writer.write(frame)
283
+ self._write_queue.task_done()
284
+ self.statistics.update_sent(frame)
285
+
286
+ # Read and process frame.
287
+ if frame := await reader.read():
288
+ self.statistics.update_received(frame)
289
+ device = await self._get_device_entry(frame.sender)
290
+ device.handle_frame(frame)
314
291
 
315
- await self._read_into_queue(reader, queues.read)
316
292
  except ProtocolError as e:
317
293
  self.statistics.failed_frames += 1
318
294
  _LOGGER.debug("Can't process received frame: %s", e)
@@ -323,36 +299,29 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
323
299
  except Exception:
324
300
  _LOGGER.exception("Unexpected exception")
325
301
 
326
- async def frame_consumer(self, queue: asyncio.Queue[Frame]) -> None:
327
- """Handle frame processing."""
328
- await self.connected.wait()
329
- while self.connected.is_set():
330
- frame = await queue.get()
331
- device = await self.get_device_entry(frame.sender)
332
- device.handle_frame(frame)
333
- queue.task_done()
334
-
335
302
  @acache
336
- async def get_device_entry(self, device_type: DeviceType) -> PhysicalDevice:
303
+ async def _get_device_entry(self, device_type: DeviceType) -> PhysicalDevice:
337
304
  """Return the device entry."""
338
305
  name = device_type.name.lower()
339
- async with self._entry_lock:
340
- if name not in self.data:
341
- device = await PhysicalDevice.create(
342
- device_type, queue=self._queues.write, network=self._network
343
- )
344
- device.dispatch_nowait(ATTR_CONNECTED, True)
345
- device.dispatch_nowait(ATTR_SETUP, True)
346
- await self.dispatch(name, device)
347
- self.statistics.update_devices(device)
348
-
349
- return self.data[name]
306
+ device = await PhysicalDevice.create(
307
+ device_type, write_queue=self._write_queue, network_info=self._network_info
308
+ )
309
+ device.dispatch_nowait(ATTR_CONNECTED, True)
310
+ device.dispatch_nowait(ATTR_SETUP, True)
311
+ await self.dispatch(name, device)
312
+ self.statistics.update_devices(device)
313
+ return device
350
314
 
351
315
  @property
352
316
  def statistics(self) -> Statistics:
353
317
  """Return the statistics."""
354
318
  return self._statistics
355
319
 
320
+ @property
321
+ def network_info(self) -> NetworkInfo:
322
+ """Return the network info."""
323
+ return self._network_info
324
+
356
325
 
357
326
  __all__ = [
358
327
  "Protocol",
@@ -22,8 +22,9 @@ from pyplumio.frames import (
22
22
  )
23
23
  from pyplumio.utils import timeout
24
24
 
25
- READER_TIMEOUT: Final = 10
26
- WRITER_TIMEOUT: Final = 10
25
+ WAIT_FOR_READ_SECONDS: Final = 10
26
+ WAIT_FOR_WRITE_SECONDS: Final = 10
27
+ FORCE_CLOSE_AFTER_SECONDS: Final = 10
27
28
 
28
29
  MIN_FRAME_LENGTH: Final = 10
29
30
  MAX_FRAME_LENGTH: Final = 1000
@@ -44,7 +45,7 @@ class FrameWriter:
44
45
  """Initialize a new frame writer."""
45
46
  self._writer = writer
46
47
 
47
- @timeout(WRITER_TIMEOUT)
48
+ @timeout(WAIT_FOR_WRITE_SECONDS)
48
49
  async def write(self, frame: Frame) -> None:
49
50
  """Send the frame and wait until send buffer is empty."""
50
51
  self._writer.write(frame.bytes)
@@ -61,7 +62,7 @@ class FrameWriter:
61
62
  "Failed to close the frame writer due to an unexpected error"
62
63
  )
63
64
 
64
- @timeout(WRITER_TIMEOUT)
65
+ @timeout(FORCE_CLOSE_AFTER_SECONDS)
65
66
  async def wait_closed(self) -> None:
66
67
  """Wait until the frame writer is closed."""
67
68
  await self._writer.wait_closed()
@@ -188,7 +189,7 @@ class FrameReader:
188
189
 
189
190
  await self._reader.read_into_buffer(MAX_FRAME_LENGTH)
190
191
 
191
- @timeout(READER_TIMEOUT)
192
+ @timeout(WAIT_FOR_READ_SECONDS)
192
193
  async def read(self) -> Frame | None:
193
194
  """Read the frame and return corresponding handler object."""
194
195
  header = await self._read_header()