PyPlumIO 0.5.51__tar.gz → 0.5.52__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 (175) hide show
  1. {pyplumio-0.5.51 → pyplumio-0.5.52}/PKG-INFO +8 -8
  2. {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/PKG-INFO +8 -8
  3. {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/requires.txt +7 -7
  4. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/index.rst +1 -1
  5. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/_version.py +2 -2
  6. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/frames/__init__.py +8 -4
  7. pyplumio-0.5.52/pyplumio/stream.py +243 -0
  8. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/alerts.py +1 -1
  9. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/schedules.py +1 -1
  10. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyproject.toml +7 -7
  11. {pyplumio-0.5.51 → pyplumio-0.5.52}/requirements.txt +1 -1
  12. {pyplumio-0.5.51 → pyplumio-0.5.52}/requirements_test.txt +6 -6
  13. pyplumio-0.5.52/tests/test_stream.py +467 -0
  14. pyplumio-0.5.51/pyplumio/stream.py +0 -158
  15. pyplumio-0.5.51/tests/test_stream.py +0 -267
  16. {pyplumio-0.5.51 → pyplumio-0.5.52}/.gitattributes +0 -0
  17. {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/CODE_OF_CONDUCT.md +0 -0
  18. {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  19. {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  20. {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/dependabot.yml +0 -0
  21. {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/workflows/ci.yml +0 -0
  22. {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/workflows/codeql.yml +0 -0
  23. {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/workflows/deploy.yml +0 -0
  24. {pyplumio-0.5.51 → pyplumio-0.5.52}/.github/workflows/documentation.yml +0 -0
  25. {pyplumio-0.5.51 → pyplumio-0.5.52}/.gitignore +0 -0
  26. {pyplumio-0.5.51 → pyplumio-0.5.52}/.pre-commit-config.yaml +0 -0
  27. {pyplumio-0.5.51 → pyplumio-0.5.52}/.qlty/qlty.toml +0 -0
  28. {pyplumio-0.5.51 → pyplumio-0.5.52}/.vscode/settings.json +0 -0
  29. {pyplumio-0.5.51 → pyplumio-0.5.52}/LICENSE +0 -0
  30. {pyplumio-0.5.51 → pyplumio-0.5.52}/MANIFEST.in +0 -0
  31. {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  32. {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  33. {pyplumio-0.5.51 → pyplumio-0.5.52}/PyPlumIO.egg-info/top_level.txt +0 -0
  34. {pyplumio-0.5.51 → pyplumio-0.5.52}/README.md +0 -0
  35. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/Makefile +0 -0
  36. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/make.bat +0 -0
  37. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/callbacks.rst +0 -0
  38. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/conf.py +0 -0
  39. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/connecting.rst +0 -0
  40. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/frames.rst +0 -0
  41. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/mixers_thermostats.rst +0 -0
  42. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/protocol.rst +0 -0
  43. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/reading.rst +0 -0
  44. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/schedules.rst +0 -0
  45. {pyplumio-0.5.51 → pyplumio-0.5.52}/docs/source/writing.rst +0 -0
  46. {pyplumio-0.5.51 → pyplumio-0.5.52}/images/ecomax.png +0 -0
  47. {pyplumio-0.5.51 → pyplumio-0.5.52}/images/rs485.png +0 -0
  48. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/__init__.py +0 -0
  49. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/__main__.py +0 -0
  50. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/connection.py +0 -0
  51. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/const.py +0 -0
  52. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/data_types.py +0 -0
  53. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/__init__.py +0 -0
  54. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/ecomax.py +0 -0
  55. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/ecoster.py +0 -0
  56. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/mixer.py +0 -0
  57. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/devices/thermostat.py +0 -0
  58. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/exceptions.py +0 -0
  59. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/filters.py +0 -0
  60. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/frames/messages.py +0 -0
  61. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/frames/requests.py +0 -0
  62. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/frames/responses.py +0 -0
  63. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/__init__.py +0 -0
  64. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/async_cache.py +0 -0
  65. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/event_manager.py +0 -0
  66. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/factory.py +0 -0
  67. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/task_manager.py +0 -0
  68. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/helpers/timeout.py +0 -0
  69. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/__init__.py +0 -0
  70. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/custom/__init__.py +0 -0
  71. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/custom/ecomax_860d3_hb.py +0 -0
  72. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/ecomax.py +0 -0
  73. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/mixer.py +0 -0
  74. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/parameters/thermostat.py +0 -0
  75. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/protocol.py +0 -0
  76. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/py.typed +0 -0
  77. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/__init__.py +0 -0
  78. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/boiler_load.py +0 -0
  79. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/boiler_power.py +0 -0
  80. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/ecomax_parameters.py +0 -0
  81. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/fan_power.py +0 -0
  82. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/frame_versions.py +0 -0
  83. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/fuel_consumption.py +0 -0
  84. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/fuel_level.py +0 -0
  85. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/lambda_sensor.py +0 -0
  86. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/mixer_parameters.py +0 -0
  87. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/mixer_sensors.py +0 -0
  88. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/modules.py +0 -0
  89. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/network_info.py +0 -0
  90. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/output_flags.py +0 -0
  91. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/outputs.py +0 -0
  92. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/pending_alerts.py +0 -0
  93. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/product_info.py +0 -0
  94. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/program_version.py +0 -0
  95. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/regulator_data.py +0 -0
  96. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/regulator_data_schema.py +0 -0
  97. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/statuses.py +0 -0
  98. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/temperatures.py +0 -0
  99. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/thermostat_parameters.py +0 -0
  100. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/structures/thermostat_sensors.py +0 -0
  101. {pyplumio-0.5.51 → pyplumio-0.5.52}/pyplumio/utils.py +0 -0
  102. {pyplumio-0.5.51 → pyplumio-0.5.52}/requirements_docs.txt +0 -0
  103. {pyplumio-0.5.51 → pyplumio-0.5.52}/setup.cfg +0 -0
  104. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/__init__.py +0 -0
  105. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/conftest.py +0 -0
  106. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/__init__.py +0 -0
  107. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_ecomax.py +0 -0
  108. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_ecoster.py +0 -0
  109. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_init.py +0 -0
  110. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_mixer.py +0 -0
  111. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/devices/test_thermostat.py +0 -0
  112. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/frames/test_init.py +0 -0
  113. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/frames/test_messages.py +0 -0
  114. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/frames/test_requests.py +0 -0
  115. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/frames/test_responses.py +0 -0
  116. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/__init__.py +0 -0
  117. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_async_cache.py +0 -0
  118. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_event_manager.py +0 -0
  119. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_factory.py +0 -0
  120. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_task_manager.py +0 -0
  121. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_timeout.py +0 -0
  122. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/helpers/test_uid.py +0 -0
  123. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/__init__.py +0 -0
  124. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/custom/__init__.py +0 -0
  125. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/custom/test_ecomax_860d3_hb.py +0 -0
  126. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/custom/test_init.py +0 -0
  127. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/test_ecomax.py +0 -0
  128. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/test_init.py +0 -0
  129. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/test_mixers.py +0 -0
  130. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/parameters/test_thermostats.py +0 -0
  131. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/ruff.toml +0 -0
  132. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/__init__.py +0 -0
  133. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_alerts.py +0 -0
  134. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_boiler_load.py +0 -0
  135. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_boiler_power.py +0 -0
  136. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_ecomax_parameters.py +0 -0
  137. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_fan_power.py +0 -0
  138. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_frame_versions.py +0 -0
  139. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_fuel_consumption.py +0 -0
  140. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_fuel_level.py +0 -0
  141. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_lambda_sensor.py +0 -0
  142. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_mixer_parameters.py +0 -0
  143. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_product_info.py +0 -0
  144. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/structures/test_schedules.py +0 -0
  145. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_connection.py +0 -0
  146. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_data_types.py +0 -0
  147. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_filters.py +0 -0
  148. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_init.py +0 -0
  149. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_main.py +0 -0
  150. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_protocol.py +0 -0
  151. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/test_utils.py +0 -0
  152. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/messages/regulator_data.json +0 -0
  153. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/messages/sensor_data.json +0 -0
  154. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/parameters/ecomax_860d3_hb.json +0 -0
  155. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/alerts.json +0 -0
  156. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/ecomax_control.json +0 -0
  157. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/ecomax_parameters.json +0 -0
  158. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/mixer_parameters.json +0 -0
  159. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  160. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  161. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/set_schedule.json +0 -0
  162. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  163. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/requests/thermostat_parameters.json +0 -0
  164. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/alerts.json +0 -0
  165. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/device_available.json +0 -0
  166. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/ecomax_parameters.json +0 -0
  167. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/mixer_parameters.json +0 -0
  168. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/password.json +0 -0
  169. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/program_version.json +0 -0
  170. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/regulator_data_schema.json +0 -0
  171. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/schedules.json +0 -0
  172. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/thermostat_parameters.json +0 -0
  173. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/responses/uid.json +0 -0
  174. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  175. {pyplumio-0.5.51 → pyplumio-0.5.52}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.51
3
+ Version: 0.5.52
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
@@ -25,17 +25,17 @@ Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: dataslots==1.2.0
27
27
  Requires-Dist: pyserial-asyncio==0.6
28
- Requires-Dist: typing-extensions==4.13.2
28
+ Requires-Dist: typing-extensions==4.14.0
29
29
  Provides-Extra: test
30
30
  Requires-Dist: codespell==2.4.1; extra == "test"
31
- Requires-Dist: coverage==7.8.0; extra == "test"
32
- Requires-Dist: freezegun==1.5.1; extra == "test"
33
- Requires-Dist: mypy==1.15.0; extra == "test"
31
+ Requires-Dist: coverage==7.9.1; extra == "test"
32
+ Requires-Dist: freezegun==1.5.2; extra == "test"
33
+ Requires-Dist: mypy==1.16.1; extra == "test"
34
34
  Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
35
35
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
36
- Requires-Dist: pytest==8.3.5; extra == "test"
37
- Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
38
- Requires-Dist: ruff==0.11.10; extra == "test"
36
+ Requires-Dist: pytest==8.4.1; extra == "test"
37
+ Requires-Dist: pytest-asyncio==1.0.0; extra == "test"
38
+ Requires-Dist: ruff==0.11.13; extra == "test"
39
39
  Requires-Dist: tox==4.26.0; extra == "test"
40
40
  Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
41
41
  Provides-Extra: docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.51
3
+ Version: 0.5.52
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
@@ -25,17 +25,17 @@ Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: dataslots==1.2.0
27
27
  Requires-Dist: pyserial-asyncio==0.6
28
- Requires-Dist: typing-extensions==4.13.2
28
+ Requires-Dist: typing-extensions==4.14.0
29
29
  Provides-Extra: test
30
30
  Requires-Dist: codespell==2.4.1; extra == "test"
31
- Requires-Dist: coverage==7.8.0; extra == "test"
32
- Requires-Dist: freezegun==1.5.1; extra == "test"
33
- Requires-Dist: mypy==1.15.0; extra == "test"
31
+ Requires-Dist: coverage==7.9.1; extra == "test"
32
+ Requires-Dist: freezegun==1.5.2; extra == "test"
33
+ Requires-Dist: mypy==1.16.1; extra == "test"
34
34
  Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
35
35
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
36
- Requires-Dist: pytest==8.3.5; extra == "test"
37
- Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
38
- Requires-Dist: ruff==0.11.10; extra == "test"
36
+ Requires-Dist: pytest==8.4.1; extra == "test"
37
+ Requires-Dist: pytest-asyncio==1.0.0; extra == "test"
38
+ Requires-Dist: ruff==0.11.13; extra == "test"
39
39
  Requires-Dist: tox==4.26.0; extra == "test"
40
40
  Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
41
41
  Provides-Extra: docs
@@ -1,6 +1,6 @@
1
1
  dataslots==1.2.0
2
2
  pyserial-asyncio==0.6
3
- typing-extensions==4.13.2
3
+ typing-extensions==4.14.0
4
4
 
5
5
  [dev]
6
6
  pyplumio[docs,test]
@@ -14,13 +14,13 @@ readthedocs-sphinx-search==0.3.2
14
14
 
15
15
  [test]
16
16
  codespell==2.4.1
17
- coverage==7.8.0
18
- freezegun==1.5.1
19
- mypy==1.15.0
17
+ coverage==7.9.1
18
+ freezegun==1.5.2
19
+ mypy==1.16.1
20
20
  numpy<3.0.0,>=2.0.0
21
21
  pyserial-asyncio-fast==0.16
22
- pytest==8.3.5
23
- pytest-asyncio==0.26.0
24
- ruff==0.11.10
22
+ pytest==8.4.1
23
+ pytest-asyncio==1.0.0
24
+ ruff==0.11.13
25
25
  tox==4.26.0
26
26
  types-pyserial==3.5.0.20250326
@@ -6,7 +6,7 @@
6
6
  Welcome to PyPlumIO's documentation!
7
7
  ====================================
8
8
 
9
- The `PyPlumIO <https://github.com/denpamusic/PyPlumIO/>`_ projects aims to
9
+ The `PyPlumIO <https://github.com/denpamusic/PyPlumIO/>`_ project aims to
10
10
  provide complete and easy to use solution for communicating with
11
11
  climate devices manufactured by `Plum Sp. z o.o. <https://www.plum.pl/>`_
12
12
 
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.5.51'
21
- __version_tuple__ = version_tuple = (0, 5, 51)
20
+ __version__ = version = '0.5.52'
21
+ __version_tuple__ = version_tuple = (0, 5, 52)
@@ -15,10 +15,14 @@ from pyplumio.utils import ensure_dict, to_camelcase
15
15
 
16
16
  FRAME_START: Final = 0x68
17
17
  FRAME_END: Final = 0x16
18
- HEADER_OFFSET: Final = 0
18
+
19
+
20
+ HEADER_INDEX: Final = 0
19
21
  FRAME_TYPE_SIZE: Final = 1
20
- CRC_SIZE: Final = 1
22
+ BCC_SIZE: Final = 1
23
+ BCC_INDEX: Final = -2
21
24
  DELIMITER_SIZE: Final = 1
25
+
22
26
  ECONET_TYPE: Final = 48
23
27
  ECONET_VERSION: Final = 5
24
28
 
@@ -199,7 +203,7 @@ class Frame(ABC):
199
203
  struct_header.size
200
204
  + FRAME_TYPE_SIZE
201
205
  + len(self.message)
202
- + CRC_SIZE
206
+ + BCC_SIZE
203
207
  + DELIMITER_SIZE
204
208
  )
205
209
 
@@ -209,7 +213,7 @@ class Frame(ABC):
209
213
  buffer = bytearray(struct_header.size)
210
214
  struct_header.pack_into(
211
215
  buffer,
212
- HEADER_OFFSET,
216
+ HEADER_INDEX,
213
217
  FRAME_START,
214
218
  self.length,
215
219
  int(self.recipient),
@@ -0,0 +1,243 @@
1
+ """Contains a frame reader and writer classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from asyncio import IncompleteReadError, StreamReader, StreamWriter
7
+ import logging
8
+ from typing import Final, NamedTuple, SupportsIndex
9
+
10
+ from pyplumio.const import DeviceType
11
+ from pyplumio.devices import is_known_device_type
12
+ from pyplumio.exceptions import ChecksumError, ReadError, UnknownDeviceError
13
+ from pyplumio.frames import (
14
+ BCC_INDEX,
15
+ DELIMITER_SIZE,
16
+ FRAME_START,
17
+ FRAME_TYPE_SIZE,
18
+ HEADER_SIZE,
19
+ Frame,
20
+ bcc,
21
+ struct_header,
22
+ )
23
+ from pyplumio.helpers.timeout import timeout
24
+
25
+ READER_TIMEOUT: Final = 10
26
+ WRITER_TIMEOUT: Final = 10
27
+
28
+ MIN_FRAME_LENGTH: Final = 10
29
+ MAX_FRAME_LENGTH: Final = 1000
30
+
31
+ DEFAULT_BUFFER_SIZE: Final = 5000
32
+
33
+ _LOGGER = logging.getLogger(__name__)
34
+
35
+
36
+ class FrameWriter:
37
+ """Represents a frame writer."""
38
+
39
+ __slots__ = ("_writer",)
40
+
41
+ _writer: StreamWriter
42
+
43
+ def __init__(self, writer: StreamWriter) -> None:
44
+ """Initialize a new frame writer."""
45
+ self._writer = writer
46
+
47
+ @timeout(WRITER_TIMEOUT)
48
+ async def write(self, frame: Frame) -> None:
49
+ """Send the frame and wait until send buffer is empty."""
50
+ self._writer.write(frame.bytes)
51
+ await self._writer.drain()
52
+ _LOGGER.debug("Sent frame: %s, bytes: %s", frame, frame.bytes)
53
+
54
+ async def close(self) -> None:
55
+ """Close the frame writer."""
56
+ try:
57
+ self._writer.close()
58
+ await self.wait_closed()
59
+ except (OSError, asyncio.TimeoutError):
60
+ _LOGGER.exception(
61
+ "Failed to close the frame writer due to an unexpected error"
62
+ )
63
+
64
+ @timeout(WRITER_TIMEOUT)
65
+ async def wait_closed(self) -> None:
66
+ """Wait until the frame writer is closed."""
67
+ await self._writer.wait_closed()
68
+
69
+
70
+ class BufferManager:
71
+ """Represents a buffered reader for reading frames."""
72
+
73
+ __slots__ = ("_buffer", "_reader")
74
+
75
+ _buffer: bytearray
76
+ _reader: StreamReader
77
+
78
+ def __init__(self, reader: StreamReader) -> None:
79
+ """Initialize a new buffered reader."""
80
+ self._buffer = bytearray()
81
+ self._reader = reader
82
+
83
+ async def ensure_buffer(self, size: int) -> None:
84
+ """Ensure the internal buffer size."""
85
+ bytes_to_read = size - len(self._buffer)
86
+ if bytes_to_read <= 0:
87
+ return None
88
+
89
+ try:
90
+ data = await self._reader.readexactly(bytes_to_read)
91
+ self._buffer.extend(data)
92
+ self.trim_to(size)
93
+ except IncompleteReadError as e:
94
+ raise ReadError(
95
+ f"Incomplete read. Tried to read {bytes_to_read} additional bytes "
96
+ f"to reach a total of {size}, but only {len(e.partial)} bytes were "
97
+ "available from stream."
98
+ ) from e
99
+ except asyncio.CancelledError:
100
+ _LOGGER.debug("Read operation cancelled while ensuring buffer")
101
+ raise
102
+ except Exception as e:
103
+ raise OSError(
104
+ f"Serial connection broken while trying to ensure {size} bytes: {e}"
105
+ ) from e
106
+
107
+ async def consume(self, size: int) -> None:
108
+ """Consume the specified number of bytes from the buffer."""
109
+ await self.ensure_buffer(size)
110
+ self._buffer = self._buffer[size:]
111
+
112
+ async def peek(self, size: int) -> bytearray:
113
+ """Read the specified number of bytes without consuming them."""
114
+ await self.ensure_buffer(size)
115
+ return self._buffer[:size]
116
+
117
+ async def read(self, size: int) -> bytearray:
118
+ """Read the bytes from buffer or stream and consume them."""
119
+ try:
120
+ return await self.peek(size)
121
+ finally:
122
+ await self.consume(size)
123
+
124
+ def seek_to(self, delimiter: SupportsIndex) -> bool:
125
+ """Trim the buffer to the first occurrence of the delimiter.
126
+
127
+ Returns True if the delimiter was found and trimmed, False otherwise.
128
+ """
129
+ if not self._buffer or (index := self._buffer.find(delimiter)) == -1:
130
+ return False
131
+
132
+ self._buffer = self._buffer[index:]
133
+ return True
134
+
135
+ def trim_to(self, size: int) -> None:
136
+ """Trim buffer to size."""
137
+ if len(self._buffer) > size:
138
+ self._buffer = self._buffer[-size:]
139
+
140
+ async def fill(self) -> None:
141
+ """Fill the buffer with data from the stream."""
142
+ try:
143
+ chunk = await self._reader.read(MAX_FRAME_LENGTH)
144
+ except asyncio.CancelledError:
145
+ _LOGGER.debug("Read operation cancelled while filling read buffer.")
146
+ raise
147
+ except Exception as e:
148
+ raise OSError(
149
+ f"Serial connection broken while filling read buffer: {e}"
150
+ ) from e
151
+
152
+ if not chunk:
153
+ _LOGGER.debug("Stream ended while filling read buffer.")
154
+ raise OSError(
155
+ "Serial connection broken: stream ended while filling read buffer"
156
+ )
157
+
158
+ self._buffer.extend(chunk)
159
+ self.trim_to(DEFAULT_BUFFER_SIZE)
160
+
161
+ @property
162
+ def buffer(self) -> bytearray:
163
+ """Return the internal buffer."""
164
+ return self._buffer
165
+
166
+
167
+ class Header(NamedTuple):
168
+ """Represents a frame header."""
169
+
170
+ frame_length: int
171
+ recipient: int
172
+ sender: int
173
+ econet_type: int
174
+ econet_version: int
175
+
176
+
177
+ class FrameReader:
178
+ """Represents a frame reader."""
179
+
180
+ __slots__ = ("_buffer",)
181
+
182
+ _buffer: BufferManager
183
+
184
+ def __init__(self, reader: StreamReader) -> None:
185
+ """Initialize a new frame reader."""
186
+ self._buffer = BufferManager(reader)
187
+
188
+ async def _read_header(self) -> Header:
189
+ """Locate and read a frame header."""
190
+ while True:
191
+ if self._buffer.seek_to(FRAME_START):
192
+ header_bytes = await self._buffer.peek(HEADER_SIZE)
193
+ return Header(*struct_header.unpack_from(header_bytes)[DELIMITER_SIZE:])
194
+
195
+ await self._buffer.fill()
196
+
197
+ @timeout(READER_TIMEOUT)
198
+ async def read(self) -> Frame | None:
199
+ """Read the frame and return corresponding handler object."""
200
+ header = await self._read_header()
201
+ frame_length, recipient, sender, econet_type, econet_version = header
202
+
203
+ if frame_length > MAX_FRAME_LENGTH or frame_length < MIN_FRAME_LENGTH:
204
+ await self._buffer.consume(HEADER_SIZE)
205
+ raise ReadError(
206
+ f"Unexpected frame length ({frame_length}), expected between "
207
+ f"{MIN_FRAME_LENGTH} and {MAX_FRAME_LENGTH}"
208
+ )
209
+
210
+ frame_bytes = await self._buffer.peek(frame_length)
211
+ checksum = bcc(frame_bytes[:BCC_INDEX])
212
+ if checksum != frame_bytes[BCC_INDEX]:
213
+ await self._buffer.consume(HEADER_SIZE)
214
+ raise ChecksumError(
215
+ f"Incorrect frame checksum: calculated {checksum}, "
216
+ f"expected {frame_bytes[BCC_INDEX]}. Frame data: {frame_bytes.hex()}"
217
+ )
218
+
219
+ await self._buffer.consume(frame_length)
220
+ if recipient not in (DeviceType.ECONET, DeviceType.ALL):
221
+ _LOGGER.debug(
222
+ "Skipping frame intended for different recipient (%s)", recipient
223
+ )
224
+ return None
225
+
226
+ if not is_known_device_type(sender):
227
+ raise UnknownDeviceError(f"Unknown sender type ({sender})")
228
+
229
+ payload_bytes = frame_bytes[HEADER_SIZE:BCC_INDEX]
230
+ frame = await Frame.create(
231
+ frame_type=payload_bytes[0],
232
+ recipient=DeviceType(recipient),
233
+ sender=DeviceType(sender),
234
+ econet_type=econet_type,
235
+ econet_version=econet_version,
236
+ message=payload_bytes[FRAME_TYPE_SIZE:],
237
+ )
238
+ _LOGGER.debug("Received frame: %s, bytes: %s", frame, frame_bytes.hex())
239
+
240
+ return frame
241
+
242
+
243
+ __all__ = ["FrameReader", "FrameWriter"]
@@ -121,4 +121,4 @@ class AlertsStructure(StructureDecoder):
121
121
  )
122
122
 
123
123
 
124
- __all__ = ["AlertsStructure", "Alert"]
124
+ __all__ = ["AlertsStructure", "Alert", "ATTR_ALERTS", "ATTR_TOTAL_ALERTS"]
@@ -175,7 +175,7 @@ def get_time(
175
175
  ) -> Time:
176
176
  """Return time for a specific index."""
177
177
  time_dt = start + (step * index)
178
- return time_dt.strftime(TIME_FORMAT)
178
+ return f"{time_dt.hour:02d}:{time_dt.minute:02d}"
179
179
 
180
180
 
181
181
  @lru_cache(maxsize=10)
@@ -26,7 +26,7 @@ classifiers = [
26
26
  dependencies = [
27
27
  "dataslots==1.2.0",
28
28
  "pyserial-asyncio==0.6",
29
- "typing-extensions==4.13.2"
29
+ "typing-extensions==4.14.0"
30
30
  ]
31
31
  dynamic = ["version"]
32
32
 
@@ -38,14 +38,14 @@ dynamic = ["version"]
38
38
  [project.optional-dependencies]
39
39
  test = [
40
40
  "codespell==2.4.1",
41
- "coverage==7.8.0",
42
- "freezegun==1.5.1",
43
- "mypy==1.15.0",
41
+ "coverage==7.9.1",
42
+ "freezegun==1.5.2",
43
+ "mypy==1.16.1",
44
44
  "numpy<3.0.0,>=2.0.0",
45
45
  "pyserial-asyncio-fast==0.16",
46
- "pytest==8.3.5",
47
- "pytest-asyncio==0.26.0",
48
- "ruff==0.11.10",
46
+ "pytest==8.4.1",
47
+ "pytest-asyncio==1.0.0",
48
+ "ruff==0.11.13",
49
49
  "tox==4.26.0",
50
50
  "types-pyserial==3.5.0.20250326"
51
51
  ]
@@ -1,3 +1,3 @@
1
1
  dataslots==1.2.0
2
2
  pyserial-asyncio==0.6
3
- typing-extensions==4.13.2
3
+ typing-extensions==4.14.0
@@ -1,13 +1,13 @@
1
1
  codespell==2.4.1
2
- coverage==7.8.0
3
- freezegun==1.5.1
4
- mypy==1.15.0
2
+ coverage==7.9.1
3
+ freezegun==1.5.2
4
+ mypy==1.16.1
5
5
  numpy<3.0.0,>=2.0.0
6
6
  pre-commit==4.2.0
7
7
  pyserial-asyncio-fast==0.16
8
- pytest-asyncio==0.26.0
9
- pytest==8.3.5
10
- ruff==0.11.10
8
+ pytest-asyncio==1.0.0
9
+ pytest==8.4.1
10
+ ruff==0.11.13
11
11
  tomli==2.2.1
12
12
  tox==4.26.0
13
13
  types-pyserial==3.5.0.20250326