PyPlumIO 0.5.34__tar.gz → 0.5.36__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.34 → pyplumio-0.5.36}/PKG-INFO +6 -6
  2. {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/PKG-INFO +6 -6
  3. {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/requires.txt +5 -5
  4. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/__init__.py +1 -0
  5. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/_version.py +2 -2
  6. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/connection.py +1 -1
  7. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/__init__.py +9 -8
  8. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/ecomax.py +32 -24
  9. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/mixer.py +4 -7
  10. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/exceptions.py +11 -0
  11. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/parameter.py +42 -47
  12. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/schedule.py +2 -3
  13. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/uid.py +5 -5
  14. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyproject.toml +5 -5
  15. {pyplumio-0.5.34 → pyplumio-0.5.36}/requirements_test.txt +5 -5
  16. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_parameter.py +13 -7
  17. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_devices.py +9 -3
  18. {pyplumio-0.5.34 → pyplumio-0.5.36}/.gitattributes +0 -0
  19. {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/CODE_OF_CONDUCT.md +0 -0
  20. {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  21. {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  22. {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/dependabot.yml +0 -0
  23. {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/workflows/ci.yml +0 -0
  24. {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/workflows/codeql-analysis.yml +0 -0
  25. {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/workflows/deploy.yml +0 -0
  26. {pyplumio-0.5.34 → pyplumio-0.5.36}/.github/workflows/documentation.yml +0 -0
  27. {pyplumio-0.5.34 → pyplumio-0.5.36}/.gitignore +0 -0
  28. {pyplumio-0.5.34 → pyplumio-0.5.36}/.pre-commit-config.yaml +0 -0
  29. {pyplumio-0.5.34 → pyplumio-0.5.36}/.vscode/settings.json +0 -0
  30. {pyplumio-0.5.34 → pyplumio-0.5.36}/LICENSE +0 -0
  31. {pyplumio-0.5.34 → pyplumio-0.5.36}/MANIFEST.in +0 -0
  32. {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  33. {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  34. {pyplumio-0.5.34 → pyplumio-0.5.36}/PyPlumIO.egg-info/top_level.txt +0 -0
  35. {pyplumio-0.5.34 → pyplumio-0.5.36}/README.md +0 -0
  36. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/Makefile +0 -0
  37. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/make.bat +0 -0
  38. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/callbacks.rst +0 -0
  39. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/conf.py +0 -0
  40. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/connecting.rst +0 -0
  41. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/frames.rst +0 -0
  42. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/index.rst +0 -0
  43. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/mixers_thermostats.rst +0 -0
  44. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/protocol.rst +0 -0
  45. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/reading.rst +0 -0
  46. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/schedules.rst +0 -0
  47. {pyplumio-0.5.34 → pyplumio-0.5.36}/docs/source/writing.rst +0 -0
  48. {pyplumio-0.5.34 → pyplumio-0.5.36}/images/ecomax.png +0 -0
  49. {pyplumio-0.5.34 → pyplumio-0.5.36}/images/rs485.png +0 -0
  50. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/__main__.py +0 -0
  51. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/const.py +0 -0
  52. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/ecoster.py +0 -0
  53. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/devices/thermostat.py +0 -0
  54. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/filters.py +0 -0
  55. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/frames/__init__.py +0 -0
  56. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/frames/messages.py +0 -0
  57. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/frames/requests.py +65 -65
  58. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/frames/responses.py +62 -62
  59. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/__init__.py +0 -0
  60. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/data_types.py +0 -0
  61. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/event_manager.py +0 -0
  62. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/factory.py +0 -0
  63. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/task_manager.py +0 -0
  64. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/helpers/timeout.py +0 -0
  65. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/protocol.py +0 -0
  66. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/py.typed +0 -0
  67. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/stream.py +0 -0
  68. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/__init__.py +0 -0
  69. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/alerts.py +0 -0
  70. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/boiler_load.py +0 -0
  71. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/boiler_power.py +0 -0
  72. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/ecomax_parameters.py +0 -0
  73. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/fan_power.py +0 -0
  74. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/frame_versions.py +0 -0
  75. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/fuel_consumption.py +0 -0
  76. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/fuel_level.py +0 -0
  77. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/lambda_sensor.py +0 -0
  78. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/mixer_parameters.py +0 -0
  79. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/mixer_sensors.py +0 -0
  80. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/modules.py +0 -0
  81. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/network_info.py +0 -0
  82. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/output_flags.py +0 -0
  83. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/outputs.py +0 -0
  84. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/pending_alerts.py +0 -0
  85. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/product_info.py +0 -0
  86. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/program_version.py +0 -0
  87. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/regulator_data.py +0 -0
  88. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/regulator_data_schema.py +0 -0
  89. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/schedules.py +0 -0
  90. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/statuses.py +0 -0
  91. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/temperatures.py +0 -0
  92. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/thermostat_parameters.py +0 -0
  93. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/structures/thermostat_sensors.py +0 -0
  94. {pyplumio-0.5.34 → pyplumio-0.5.36}/pyplumio/utils.py +0 -0
  95. {pyplumio-0.5.34 → pyplumio-0.5.36}/requirements.txt +0 -0
  96. {pyplumio-0.5.34 → pyplumio-0.5.36}/requirements_docs.txt +0 -0
  97. {pyplumio-0.5.34 → pyplumio-0.5.36}/setup.cfg +0 -0
  98. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/__init__.py +0 -0
  99. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/conftest.py +0 -0
  100. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/frames/test_init.py +0 -0
  101. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/frames/test_messages.py +0 -0
  102. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/frames/test_requests.py +0 -0
  103. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/frames/test_responses.py +0 -0
  104. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/__init__.py +0 -0
  105. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_data_types.py +0 -0
  106. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_event_manager.py +0 -0
  107. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_factory.py +0 -0
  108. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_schedule.py +0 -0
  109. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_task_manager.py +0 -0
  110. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_timeout.py +0 -0
  111. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/helpers/test_uid.py +0 -0
  112. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/ruff.toml +0 -0
  113. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_connection.py +0 -0
  114. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_filters.py +0 -0
  115. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_init.py +0 -0
  116. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_main.py +0 -0
  117. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_protocol.py +0 -0
  118. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_stream.py +0 -0
  119. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/test_utils.py +0 -0
  120. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/messages/regulator_data.json +0 -0
  121. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/messages/sensor_data.json +0 -0
  122. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/alerts.json +0 -0
  123. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/ecomax_control.json +0 -0
  124. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/ecomax_parameters.json +0 -0
  125. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/mixer_parameters.json +0 -0
  126. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  127. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  128. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/set_schedule.json +0 -0
  129. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  130. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/requests/thermostat_parameters.json +0 -0
  131. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/alerts.json +0 -0
  132. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/device_available.json +0 -0
  133. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/ecomax_parameters.json +0 -0
  134. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/mixer_parameters.json +0 -0
  135. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/password.json +0 -0
  136. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/program_version.json +0 -0
  137. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/regulator_data_schema.json +0 -0
  138. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/schedules.json +0 -0
  139. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/thermostat_parameters.json +0 -0
  140. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/responses/uid.json +0 -0
  141. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  142. {pyplumio-0.5.34 → pyplumio-0.5.36}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyPlumIO
3
- Version: 0.5.34
3
+ Version: 0.5.36
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,22 +27,22 @@ Requires-Dist: dataslots==1.2.0
27
27
  Requires-Dist: pyserial-asyncio==0.6
28
28
  Requires-Dist: typing-extensions==4.12.2
29
29
  Provides-Extra: test
30
- Requires-Dist: codespell==2.3.0; extra == "test"
30
+ Requires-Dist: codespell==2.4.0; extra == "test"
31
31
  Requires-Dist: coverage==7.6.10; extra == "test"
32
32
  Requires-Dist: mypy==1.14.1; extra == "test"
33
33
  Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
34
34
  Requires-Dist: pytest==8.3.4; extra == "test"
35
35
  Requires-Dist: pytest-asyncio==0.25.2; extra == "test"
36
- Requires-Dist: ruff==0.9.2; extra == "test"
37
- Requires-Dist: tox==4.23.2; extra == "test"
38
- Requires-Dist: types-pyserial==3.5.0.20241221; extra == "test"
36
+ Requires-Dist: ruff==0.9.3; extra == "test"
37
+ Requires-Dist: tox==4.24.1; extra == "test"
38
+ Requires-Dist: types-pyserial==3.5.0.20250124; extra == "test"
39
39
  Provides-Extra: docs
40
40
  Requires-Dist: sphinx==8.1.3; extra == "docs"
41
41
  Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
42
42
  Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
43
43
  Provides-Extra: dev
44
44
  Requires-Dist: pyplumio[docs,test]; extra == "dev"
45
- Requires-Dist: pre-commit==4.0.1; extra == "dev"
45
+ Requires-Dist: pre-commit==4.1.0; extra == "dev"
46
46
  Requires-Dist: tomli==2.2.1; extra == "dev"
47
47
 
48
48
  # PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyPlumIO
3
- Version: 0.5.34
3
+ Version: 0.5.36
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,22 +27,22 @@ Requires-Dist: dataslots==1.2.0
27
27
  Requires-Dist: pyserial-asyncio==0.6
28
28
  Requires-Dist: typing-extensions==4.12.2
29
29
  Provides-Extra: test
30
- Requires-Dist: codespell==2.3.0; extra == "test"
30
+ Requires-Dist: codespell==2.4.0; extra == "test"
31
31
  Requires-Dist: coverage==7.6.10; extra == "test"
32
32
  Requires-Dist: mypy==1.14.1; extra == "test"
33
33
  Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
34
34
  Requires-Dist: pytest==8.3.4; extra == "test"
35
35
  Requires-Dist: pytest-asyncio==0.25.2; extra == "test"
36
- Requires-Dist: ruff==0.9.2; extra == "test"
37
- Requires-Dist: tox==4.23.2; extra == "test"
38
- Requires-Dist: types-pyserial==3.5.0.20241221; extra == "test"
36
+ Requires-Dist: ruff==0.9.3; extra == "test"
37
+ Requires-Dist: tox==4.24.1; extra == "test"
38
+ Requires-Dist: types-pyserial==3.5.0.20250124; extra == "test"
39
39
  Provides-Extra: docs
40
40
  Requires-Dist: sphinx==8.1.3; extra == "docs"
41
41
  Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
42
42
  Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
43
43
  Provides-Extra: dev
44
44
  Requires-Dist: pyplumio[docs,test]; extra == "dev"
45
- Requires-Dist: pre-commit==4.0.1; extra == "dev"
45
+ Requires-Dist: pre-commit==4.1.0; extra == "dev"
46
46
  Requires-Dist: tomli==2.2.1; extra == "dev"
47
47
 
48
48
  # PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
@@ -4,7 +4,7 @@ typing-extensions==4.12.2
4
4
 
5
5
  [dev]
6
6
  pyplumio[docs,test]
7
- pre-commit==4.0.1
7
+ pre-commit==4.1.0
8
8
  tomli==2.2.1
9
9
 
10
10
  [docs]
@@ -13,12 +13,12 @@ sphinx_rtd_theme==3.0.2
13
13
  readthedocs-sphinx-search==0.3.2
14
14
 
15
15
  [test]
16
- codespell==2.3.0
16
+ codespell==2.4.0
17
17
  coverage==7.6.10
18
18
  mypy==1.14.1
19
19
  pyserial-asyncio-fast==0.14
20
20
  pytest==8.3.4
21
21
  pytest-asyncio==0.25.2
22
- ruff==0.9.2
23
- tox==4.23.2
24
- types-pyserial==3.5.0.20241221
22
+ ruff==0.9.3
23
+ tox==4.24.1
24
+ types-pyserial==3.5.0.20250124
@@ -101,6 +101,7 @@ __all__ = [
101
101
  "ProtocolError",
102
102
  "PyPlumIOError",
103
103
  "ReadError",
104
+ "RequestError",
104
105
  "SerialConnection",
105
106
  "TcpConnection",
106
107
  "UnknownDeviceError",
@@ -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.34'
16
- __version_tuple__ = version_tuple = (0, 5, 34)
15
+ __version__ = version = '0.5.36'
16
+ __version_tuple__ = version_tuple = (0, 5, 36)
@@ -86,7 +86,7 @@ class Connection(ABC, TaskManager):
86
86
  RECONNECT_TIMEOUT,
87
87
  )
88
88
  await asyncio.sleep(RECONNECT_TIMEOUT)
89
- self.create_task(self._reconnect())
89
+ self.create_task(self._reconnect(), name="reconnect_task")
90
90
 
91
91
  async def connect(self) -> None:
92
92
  """Open the connection.
@@ -9,11 +9,11 @@ import logging
9
9
  from typing import Any, ClassVar
10
10
 
11
11
  from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType
12
- from pyplumio.exceptions import UnknownDeviceError
12
+ from pyplumio.exceptions import RequestError, UnknownDeviceError
13
13
  from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
14
14
  from pyplumio.helpers.event_manager import EventManager
15
15
  from pyplumio.helpers.factory import create_instance
16
- from pyplumio.helpers.parameter import Parameter, ParameterValue
16
+ from pyplumio.helpers.parameter import NumericType, Parameter, State
17
17
  from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
18
18
  from pyplumio.structures.network_info import NetworkInfo
19
19
  from pyplumio.utils import to_camelcase
@@ -57,7 +57,7 @@ class Device(ABC, EventManager):
57
57
  async def set(
58
58
  self,
59
59
  name: str,
60
- value: ParameterValue,
60
+ value: NumericType | State | bool,
61
61
  retries: int = 5,
62
62
  timeout: float | None = None,
63
63
  ) -> bool:
@@ -89,7 +89,7 @@ class Device(ABC, EventManager):
89
89
  def set_nowait(
90
90
  self,
91
91
  name: str,
92
- value: ParameterValue,
92
+ value: NumericType | State | bool,
93
93
  retries: int = 5,
94
94
  timeout: float | None = None,
95
95
  ) -> None:
@@ -180,7 +180,7 @@ class PhysicalDevice(Device, ABC):
180
180
  )
181
181
 
182
182
  errors = [
183
- result.args[1] for result in results if isinstance(result, BaseException)
183
+ result.frame_type for result in results if isinstance(result, RequestError)
184
184
  ]
185
185
 
186
186
  await asyncio.gather(
@@ -203,9 +203,10 @@ class PhysicalDevice(Device, ABC):
203
203
  except asyncio.TimeoutError:
204
204
  retries -= 1
205
205
 
206
- raise ValueError(
207
- f"Failed to request parameter '{name}' with frame type '{frame_type}' "
208
- f"after {retries} retries."
206
+ raise RequestError(
207
+ f"Failed to request '{name}' with frame type '{frame_type}' after "
208
+ f"{retries} retries.",
209
+ frame_type=frame_type,
209
210
  )
210
211
 
211
212
  @classmethod
@@ -59,7 +59,8 @@ ATTR_MIXERS: Final = "mixers"
59
59
  ATTR_THERMOSTATS: Final = "thermostats"
60
60
  ATTR_FUEL_BURNED: Final = "fuel_burned"
61
61
 
62
- MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * 1000000000
62
+ NANOSECONDS_IN_SECOND: Final = 1_000_000_000
63
+ MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * NANOSECONDS_IN_SECOND
63
64
 
64
65
  SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
65
66
  DataFrameDescription(
@@ -105,16 +106,15 @@ class EcoMAX(PhysicalDevice):
105
106
  """Represents an ecoMAX controller."""
106
107
 
107
108
  address = DeviceType.ECOMAX
108
-
109
- _fuel_burned_timestamp_ns: int
110
109
  _setup_frames = SETUP_FRAME_TYPES
111
110
 
111
+ _fuel_burned_time_ns: int
112
+
112
113
  def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
113
114
  """Initialize a new ecoMAX controller."""
114
115
  super().__init__(queue, network)
115
- self._fuel_burned_timestamp_ns = time.perf_counter_ns()
116
116
  self.subscribe(ATTR_ECOMAX_PARAMETERS, self._handle_ecomax_parameters)
117
- self.subscribe(ATTR_FUEL_CONSUMPTION, self._add_burned_fuel_counter)
117
+ self.subscribe(ATTR_FUEL_CONSUMPTION, self._add_burned_fuel_meter)
118
118
  self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
119
119
  self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
120
120
  self.subscribe(ATTR_SCHEDULES, self._add_schedules)
@@ -124,6 +124,7 @@ class EcoMAX(PhysicalDevice):
124
124
  self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_thermostat_parameters)
125
125
  self.subscribe(ATTR_THERMOSTAT_PROFILE, self._add_thermostat_profile_parameter)
126
126
  self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
127
+ self._fuel_burned_time_ns = time.perf_counter_ns()
127
128
 
128
129
  async def async_setup(self) -> bool:
129
130
  """Set up an ecoMAX controller."""
@@ -183,13 +184,10 @@ class EcoMAX(PhysicalDevice):
183
184
  description = ECOMAX_PARAMETERS[product.type][index]
184
185
  except IndexError:
185
186
  _LOGGER.warning(
186
- (
187
- "Encountered unknown ecoMAX parameter (%i): %s. "
188
- "Your device isn't fully compatible with this software and "
189
- "may not work properly. "
190
- "Please visit the issue tracker and open a feature "
191
- "request to support %s"
192
- ),
187
+ "Encountered unknown ecoMAX parameter (%i): %s. "
188
+ "Your device isn't fully compatible with this software "
189
+ "and may not work properly. Please visit the issue tracker "
190
+ "and open a feature request to support %s",
193
191
  index,
194
192
  values,
195
193
  product.model,
@@ -211,19 +209,29 @@ class EcoMAX(PhysicalDevice):
211
209
  await asyncio.gather(*_ecomax_parameter_events())
212
210
  return True
213
211
 
214
- async def _add_burned_fuel_counter(self, fuel_consumption: float) -> None:
215
- """Calculate fuel burned since last sensor's data message."""
216
- current_timestamp_ns = time.perf_counter_ns()
217
- time_passed_ns = current_timestamp_ns - self._fuel_burned_timestamp_ns
218
- self._fuel_burned_timestamp_ns = current_timestamp_ns
219
- if time_passed_ns >= MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
220
- _LOGGER.warning(
221
- "Skipping outdated fuel consumption data, was %i seconds old",
222
- time_passed_ns / 1000000000,
212
+ async def _add_burned_fuel_meter(self, fuel_consumption: float) -> None:
213
+ """Calculate and dispatch the amount of fuel burned.
214
+
215
+ This method calculates the fuel burned based on the time
216
+ elapsed since the last sensor message, which contains fuel
217
+ consumption data. If the elapsed time is within the acceptable
218
+ range, it dispatches the fuel burned data. Otherwise, it logs a
219
+ warning and skips the outdated data.
220
+ """
221
+ time_ns = time.perf_counter_ns()
222
+ nanoseconds_passed = time_ns - self._fuel_burned_time_ns
223
+ self._fuel_burned_time_ns = time_ns
224
+ if nanoseconds_passed < MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
225
+ return self.dispatch_nowait(
226
+ ATTR_FUEL_BURNED,
227
+ fuel_consumption * nanoseconds_passed / (3600 * NANOSECONDS_IN_SECOND),
223
228
  )
224
- else:
225
- fuel_burned = fuel_consumption * time_passed_ns / (3600 * 1000000000)
226
- await self.dispatch(ATTR_FUEL_BURNED, fuel_burned)
229
+
230
+ _LOGGER.warning(
231
+ "Skipping outdated fuel consumption data: %f (was %i seconds old)",
232
+ fuel_consumption,
233
+ nanoseconds_passed / NANOSECONDS_IN_SECOND,
234
+ )
227
235
 
228
236
  async def _handle_mixer_parameters(
229
237
  self,
@@ -64,13 +64,10 @@ class Mixer(VirtualDevice):
64
64
  description = MIXER_PARAMETERS[product.type][index]
65
65
  except IndexError:
66
66
  _LOGGER.warning(
67
- (
68
- "Encountered unknown mixer parameter (%i): %s. "
69
- "Your device isn't fully compatible with this software and "
70
- "may not work properly. "
71
- "Please visit the issue tracker and open a feature "
72
- "request to support %s"
73
- ),
67
+ "Encountered unknown mixer parameter (%i): %s. "
68
+ "Your device isn't fully compatible with this software "
69
+ "and may not work properly. Please visit the issue tracker "
70
+ "and open a feature request to support %s",
74
71
  index,
75
72
  values,
76
73
  product.model,
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from pyplumio.const import FrameType
6
+
5
7
 
6
8
  class PyPlumIOError(Exception):
7
9
  """Base PyPlumIO error class."""
@@ -11,6 +13,15 @@ class ConnectionFailedError(PyPlumIOError):
11
13
  """Raised on connection failure."""
12
14
 
13
15
 
16
+ class RequestError(PyPlumIOError):
17
+ """Raised on request error."""
18
+
19
+ def __init__(self, message: str, frame_type: FrameType) -> None:
20
+ """Initialize a new RequestError."""
21
+ super().__init__(message)
22
+ self.frame_type = frame_type
23
+
24
+
14
25
  class ProtocolError(PyPlumIOError):
15
26
  """Base class for protocol-related errors."""
16
27
 
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
6
6
  import asyncio
7
7
  from dataclasses import dataclass
8
8
  import logging
9
- from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union
9
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
10
10
 
11
11
  from dataslots import dataslots
12
12
  from typing_extensions import TypeAlias
@@ -19,8 +19,8 @@ if TYPE_CHECKING:
19
19
 
20
20
  _LOGGER = logging.getLogger(__name__)
21
21
 
22
-
23
- ParameterValue: TypeAlias = Union[int, float, bool, Literal["off", "on"]]
22
+ NumericType: TypeAlias = Union[int, float]
23
+ State: TypeAlias = Literal["on", "off"]
24
24
  ParameterT = TypeVar("ParameterT", bound="Parameter")
25
25
 
26
26
 
@@ -28,7 +28,7 @@ def unpack_parameter(
28
28
  data: bytearray, offset: int = 0, size: int = 1
29
29
  ) -> ParameterValues | None:
30
30
  """Unpack a device parameter."""
31
- if not check_parameter(data[offset : offset + size * 3]):
31
+ if not validate_parameter(data[offset : offset + size * 3]):
32
32
  return None
33
33
 
34
34
  value = data[offset : offset + size]
@@ -42,14 +42,17 @@ def unpack_parameter(
42
42
  )
43
43
 
44
44
 
45
- def check_parameter(data: bytearray) -> bool:
45
+ def validate_parameter(data: bytearray) -> bool:
46
46
  """Check if parameter contains any bytes besides 0xFF."""
47
47
  return any(x for x in data if x != BYTE_UNDEFINED)
48
48
 
49
49
 
50
- def _normalize_parameter_value(value: ParameterValue) -> int:
51
- """Normalize a parameter value."""
52
- if value in (STATE_OFF, STATE_ON):
50
+ def parameter_value_to_int(value: NumericType | State | bool) -> int:
51
+ """Convert a parameter value to an integer.
52
+
53
+ If the value is STATE_OFF or STATE_ON, it returns 0 or 1 respectively.
54
+ """
55
+ if value in get_args(State):
53
56
  return 1 if value == STATE_ON else 0
54
57
 
55
58
  return int(value)
@@ -77,19 +80,11 @@ class ParameterDescription:
77
80
  class Parameter(ABC):
78
81
  """Represents a base parameter."""
79
82
 
80
- __slots__ = (
81
- "device",
82
- "description",
83
- "_pending_update",
84
- "_previous_value",
85
- "_index",
86
- "_values",
87
- )
83
+ __slots__ = ("device", "description", "_pending_update", "_index", "_values")
88
84
 
89
85
  device: Device
90
86
  description: ParameterDescription
91
87
  _pending_update: bool
92
- _previous_value: int
93
88
  _index: int
94
89
  _values: ParameterValues
95
90
 
@@ -103,8 +98,8 @@ class Parameter(ABC):
103
98
  """Initialize a new parameter."""
104
99
  self.device = device
105
100
  self.description = description
101
+ self._index = index
106
102
  self._pending_update = False
107
- self._previous_value = 0
108
103
  self._index = index
109
104
  self._values = values if values else ParameterValues(0, 0, 0)
110
105
 
@@ -127,9 +122,9 @@ class Parameter(ABC):
127
122
  handler = getattr(self.values, method_to_call)
128
123
  return handler(other)
129
124
 
130
- if isinstance(other, (int, float, bool)) or other in (STATE_OFF, STATE_ON):
125
+ if isinstance(other, (int, float, bool)) or other in get_args(State):
131
126
  handler = getattr(self.values.value, method_to_call)
132
- return handler(_normalize_parameter_value(other))
127
+ return handler(parameter_value_to_int(other))
133
128
  else:
134
129
  return NotImplemented
135
130
 
@@ -184,42 +179,44 @@ class Parameter(ABC):
184
179
  )
185
180
  return type(self)(self.device, self.description, values)
186
181
 
187
- def validate(self, value: ParameterValue) -> int:
182
+ def validate(self, value: NumericType | State | bool) -> int:
188
183
  """Validate a parameter value."""
189
- value = _normalize_parameter_value(value)
190
- if value < self.values.min_value or value > self.values.max_value:
184
+ int_value = parameter_value_to_int(value)
185
+ if int_value < self.values.min_value or int_value > self.values.max_value:
191
186
  raise ValueError(
192
187
  f"Invalid value: {value}. Must be between "
193
188
  f"{self.min_value} and {self.max_value}."
194
189
  )
195
190
 
196
- return value
191
+ return int_value
197
192
 
198
193
  async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
199
194
  """Set a parameter value."""
200
- return await self._try_set(self.validate(value), retries, timeout)
195
+ return await self._attempt_update(self.validate(value), retries, timeout)
201
196
 
202
197
  def set_nowait(self, value: Any, retries: int = 5, timeout: float = 5.0) -> None:
203
198
  """Set a parameter value without waiting."""
204
- self.device.create_task(self._try_set(self.validate(value), retries, timeout))
199
+ self.device.create_task(
200
+ self._attempt_update(self.validate(value), retries, timeout)
201
+ )
205
202
 
206
- async def _try_set(
207
- self, value: Any, retries: int = 5, timeout: float = 5.0
203
+ async def _attempt_update(
204
+ self, value: int, retries: int = 5, timeout: float = 5.0
208
205
  ) -> bool:
209
- """Try to set a parameter value."""
206
+ """Attempt to update a parameter value on the remote device."""
210
207
  if value == self.values.value:
211
208
  # Value is unchanged
212
209
  return True
213
210
 
214
- self._previous_value = self._values.value
215
- self._values.value = value
216
211
  self._pending_update = True
212
+ self._values.value = value
213
+ initial_retries = retries
217
214
  while self.pending_update:
218
215
  if retries <= 0:
219
216
  _LOGGER.warning(
220
- "Failed to set parameter '%s' after %d retries",
217
+ "Unable to confirm update of '%s' parameter after %d retries",
221
218
  self.description.name,
222
- retries,
219
+ initial_retries,
223
220
  )
224
221
  return False
225
222
 
@@ -231,9 +228,7 @@ class Parameter(ABC):
231
228
 
232
229
  def update(self, values: ParameterValues) -> None:
233
230
  """Update the parameter values."""
234
- if self.pending_update and self._previous_value != values.value:
235
- self._pending_update = False
236
-
231
+ self._pending_update = False
237
232
  self._values = values
238
233
 
239
234
  @property
@@ -267,17 +262,17 @@ class Parameter(ABC):
267
262
 
268
263
  @property
269
264
  @abstractmethod
270
- def value(self) -> Any:
265
+ def value(self) -> NumericType | State | bool:
271
266
  """Return the value."""
272
267
 
273
268
  @property
274
269
  @abstractmethod
275
- def min_value(self) -> Any:
270
+ def min_value(self) -> NumericType | State | bool:
276
271
  """Return the minimum allowed value."""
277
272
 
278
273
  @property
279
274
  @abstractmethod
280
- def max_value(self) -> Any:
275
+ def max_value(self) -> NumericType | State | bool:
281
276
  """Return the maximum allowed value."""
282
277
 
283
278
  @abstractmethod
@@ -301,13 +296,13 @@ class Number(Parameter):
301
296
  description: NumberDescription
302
297
 
303
298
  async def set(
304
- self, value: int | float, retries: int = 5, timeout: float = 5.0
299
+ self, value: NumericType, retries: int = 5, timeout: float = 5.0
305
300
  ) -> bool:
306
301
  """Set a parameter value."""
307
302
  return await super().set(value, retries, timeout)
308
303
 
309
304
  def set_nowait(
310
- self, value: int | float, retries: int = 5, timeout: float = 5.0
305
+ self, value: NumericType, retries: int = 5, timeout: float = 5.0
311
306
  ) -> None:
312
307
  """Set a parameter value without waiting."""
313
308
  super().set_nowait(value, retries, timeout)
@@ -317,17 +312,17 @@ class Number(Parameter):
317
312
  return Request()
318
313
 
319
314
  @property
320
- def value(self) -> int | float:
315
+ def value(self) -> NumericType:
321
316
  """Return the value."""
322
317
  return self.values.value
323
318
 
324
319
  @property
325
- def min_value(self) -> int | float:
320
+ def min_value(self) -> NumericType:
326
321
  """Return the minimum allowed value."""
327
322
  return self.values.min_value
328
323
 
329
324
  @property
330
- def max_value(self) -> int | float:
325
+ def max_value(self) -> NumericType:
331
326
  """Return the maximum allowed value."""
332
327
  return self.values.max_value
333
328
 
@@ -351,13 +346,13 @@ class Switch(Parameter):
351
346
  description: SwitchDescription
352
347
 
353
348
  async def set(
354
- self, value: bool | Literal["off", "on"], retries: int = 5, timeout: float = 5.0
349
+ self, value: State | bool, retries: int = 5, timeout: float = 5.0
355
350
  ) -> bool:
356
351
  """Set a parameter value."""
357
352
  return await super().set(value, retries, timeout)
358
353
 
359
354
  def set_nowait(
360
- self, value: bool | Literal["off", "on"], retries: int = 5, timeout: float = 5.0
355
+ self, value: State | bool, retries: int = 5, timeout: float = 5.0
361
356
  ) -> None:
362
357
  """Set a switch value without waiting."""
363
358
  super().set_nowait(value, retries, timeout)
@@ -393,7 +388,7 @@ class Switch(Parameter):
393
388
  return Request()
394
389
 
395
390
  @property
396
- def value(self) -> Literal["off", "on"]:
391
+ def value(self) -> State:
397
392
  """Return the value."""
398
393
  return STATE_ON if self.values.value == 1 else STATE_OFF
399
394
 
@@ -21,8 +21,7 @@ TIME_FORMAT: Final = "%H:%M"
21
21
  STATE_NIGHT: Final = "night"
22
22
  STATE_DAY: Final = "day"
23
23
 
24
- ON_STATES: Final = (STATE_ON, STATE_DAY)
25
- OFF_STATES: Final = (STATE_OFF, STATE_NIGHT)
24
+ _ON_STATES: Final = {STATE_ON, STATE_DAY}
26
25
 
27
26
  ScheduleState: TypeAlias = Literal["on", "off", "day", "night"]
28
27
  Time = Annotated[str, "time in HH:MM format"]
@@ -113,7 +112,7 @@ class ScheduleDay(MutableMapping):
113
112
  )
114
113
 
115
114
  for index in _get_time_range(start, end):
116
- self._intervals[index] = True if state in ON_STATES else False
115
+ self._intervals[index] = True if state in _ON_STATES else False
117
116
 
118
117
  def set_on(self, start: Time = "00:00", end: Time = "00:00") -> None:
119
118
  """Set a schedule interval state to 'on'."""
@@ -1,4 +1,4 @@
1
- """Contains an UID helpers."""
1
+ """Contains UID helpers."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -18,18 +18,18 @@ def decode_uid(buffer: bytes) -> str:
18
18
  def _base5(buffer: bytes) -> str:
19
19
  """Encode bytes to a base5 encoded string."""
20
20
  number = int.from_bytes(buffer, byteorder="little")
21
- output = ""
21
+ output = []
22
22
  while number:
23
- output = BASE5_KEY[number & 0b00011111] + output
23
+ output.append(BASE5_KEY[number & 0b00011111])
24
24
  number >>= 5
25
25
 
26
- return output
26
+ return "".join(reversed(output))
27
27
 
28
28
 
29
29
  def _crc16(buffer: bytes) -> bytes:
30
30
  """Return a CRC 16."""
31
31
  crc16 = reduce(_crc16_byte, buffer, CRC)
32
- return crc16.to_bytes(byteorder="little", length=2)
32
+ return crc16.to_bytes(length=2, byteorder="little")
33
33
 
34
34
 
35
35
  def _crc16_byte(crc: int, byte: int) -> int:
@@ -37,15 +37,15 @@ dynamic = ["version"]
37
37
 
38
38
  [project.optional-dependencies]
39
39
  test = [
40
- "codespell==2.3.0",
40
+ "codespell==2.4.0",
41
41
  "coverage==7.6.10",
42
42
  "mypy==1.14.1",
43
43
  "pyserial-asyncio-fast==0.14",
44
44
  "pytest==8.3.4",
45
45
  "pytest-asyncio==0.25.2",
46
- "ruff==0.9.2",
47
- "tox==4.23.2",
48
- "types-pyserial==3.5.0.20241221"
46
+ "ruff==0.9.3",
47
+ "tox==4.24.1",
48
+ "types-pyserial==3.5.0.20250124"
49
49
  ]
50
50
  docs = [
51
51
  "sphinx==8.1.3",
@@ -54,7 +54,7 @@ docs = [
54
54
  ]
55
55
  dev = [
56
56
  "pyplumio[test,docs]",
57
- "pre-commit==4.0.1",
57
+ "pre-commit==4.1.0",
58
58
  "tomli==2.2.1"
59
59
  ]
60
60
 
@@ -1,11 +1,11 @@
1
- codespell==2.3.0
1
+ codespell==2.4.0
2
2
  coverage==7.6.10
3
3
  mypy==1.14.1
4
- pre-commit==4.0.1
4
+ pre-commit==4.1.0
5
5
  pyserial-asyncio-fast==0.14
6
6
  pytest-asyncio==0.25.2
7
7
  pytest==8.3.4
8
- ruff==0.9.2
8
+ ruff==0.9.3
9
9
  tomli==2.2.1
10
- tox==4.23.2
11
- types-pyserial==3.5.0.20241221
10
+ tox==4.24.1
11
+ types-pyserial==3.5.0.20250124